silentbalanceyh/vertx-zero

View on GitHub
vertx-gaia/vertx-ams/src/main/jib/io/horizon/uca/qr/syntax/IrAnalyzer.java

Summary

Maintainability
A
55 mins
Test Coverage
package io.horizon.uca.qr.syntax;

import io.horizon.eon.VString;
import io.horizon.eon.VValue;
import io.horizon.util.HUt;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;

/**
 * ## Critical Query Engine Class
 *
 * ### 1. Intro
 *
 * Provide visitor on json to process json syntax visitor especial in Tree Mode.
 *
 * ### 2. Features
 *
 * > The default operator is `OR` instead of the value `AND`.
 *
 * ```json
 * // <pre><code class="json">
 *     {
 *         "AND Condition": {
 *             "field1": "value1",
 *             "field2": "value2",
 *             "": true
 *         },
 *         "OR Condition": {
 *             "field1": "value1",
 *             "field2": "value2",
 *             "": false
 *         },
 *         "OR Default": {
 *             "field1": "value1",
 *             "field2": "value2"
 *         }
 *     }
 * // </code></pre>
 * ```
 *
 * ### 3. CRUD
 *
 * #### 3.1. Save
 *
 * When user want to `add` new condition in current criteria, there should be situation to limit the original criteria,
 * it means that the connector of this kind situation should be `AND` instead of `OR`, but except the default connector
 * provided.
 *
 * |Situation|Comment|
 * |---|:---|
 * |`{}`|No `field = value` in current input json object.|
 * |`{field1:value1}`|There is one `field = value` in current json object.|
 * |`{field1:value1,field2:value2}`|More than one `field = value` existing and the system should calculate the operator.|
 *
 * Here are additional step to append the same condition in json object here.
 *
 * > ADD, APPEND mode only.
 *
 * #### 3.2. Remove
 *
 * When user want to `remove` current condition, there should be situation of following two:
 *
 * 1. `field,op` as Qr key, match the condition fully.
 * 2. `field` as Qr field, match the condition witch start with `field`.
 *
 * #### 3.3. Update
 *
 * When user want to `update` current condition, the system visit the tree to find the matched `fieldExpr = value` first,
 * when the condition has been found, replace the original condition directly.
 *
 * 1. Replace Mode: Call `=` to replace current condition.
 * 2. Append Mode: When `i` operator, combine the condition value to `field,i = []` based on operator.
 *
 * > REPLACE mode only.
 *
 * #### 3.4. Transfer
 *
 * Input is `field2 = value2` to replace `field1 = value1` condition instead of update/save/delete.
 *
 * In this kind of situation, there may be different `field1` converted to same `field2`, then the `field2` should
 * be merged ( APPEND ) to original one.
 *
 * @author <a href="http://www.origin-x.cn">Lang</a>
 */
class IrAnalyzer implements IrDo {
    private final JsonObject raw = new JsonObject();

    IrAnalyzer(final JsonObject input) {
        this.raw.mergeIn(input, true);
    }

    /**
     * Check whether json object is complex
     *
     * 1. When any one value is `JsonObject`, it's true.
     * 2. otherwise the result is false.
     *
     * @param source {@link io.vertx.core.json.JsonObject} input json
     *
     * @return {@link java.lang.Boolean}
     */
    static boolean isComplex(final JsonObject source) {
        return source.fieldNames().stream()
            .anyMatch(field -> isJson(source.getValue(field)));
    }

    /**
     * Return true if the value type is {@link io.vertx.core.json.JsonObject}
     *
     * @param value {@link java.lang.Object} Input value that will be checked.
     *
     * @return {@link java.lang.Boolean}
     */
    private static boolean isJson(final Object value) {
        if (Objects.isNull(value)) {
            /* null dot */
            return false;
        } else {
            /* valid */
            return HUt.isJObject(value.getClass());
        }
    }

    /**
     * @param fieldExpr {@link java.lang.String}
     * @param value     {@link java.lang.Object}
     *
     * @return {@link IrDo}
     */
    @Override
    public IrDo save(final String fieldExpr, final Object value) {
        if (VString.EMPTY.equals(fieldExpr)) {
            /*
             * "" for AND / OR
             */
            this.raw.put(fieldExpr, value);
        } else {
            final IrItem item = new IrItem(fieldExpr).value(value);
            /*
             * Boolean for internal execution
             */
            final AtomicBoolean existing = new AtomicBoolean();
            existing.set(Boolean.FALSE);
            this.itExist(this.raw, item, (qr, ref) -> {
                /*
                 * Flat processing.
                 */
                existing.set(Boolean.TRUE);
                /*
                 * Save Operation
                 */
                this.saveWhere(ref, item, qr);
            });
            if (!existing.get()) {
                /*
                 * Add Operation.
                 */
                this.addWhere(this.raw, item);
            }
        }
        return this;
    }

    /**
     * @param fieldExpr {@link java.lang.String} Removed fieldExpr
     * @param fully     {@link java.lang.Boolean} Removed fully or ?
     *
     * @return {@link IrDo}
     */
    @Override
    public IrDo remove(final String fieldExpr, final boolean fully) {
        /*
         * Existing the QrItem
         */
        final IrItem item = new IrItem(fieldExpr);
        this.itExist(this.raw, (field, value) -> {
            if (fully) {
                /* Compare fieldExpr */
                return field.equals(item.qrKey());
            } else {
                /* Check whether each start with field. */
                return field.startsWith(item.field());
            }
            // Remove matched condition.
        }, (qr, ref) -> ref.remove(qr.qrKey()));
        return this;
    }

    @Override
    public void match(final String field, final BiConsumer<IrItem, JsonObject> consumer) {
        this.itExist(this.raw, (fieldExpr, value) -> fieldExpr.startsWith(field), consumer);
    }

    /**
     * 1. REPLACE
     *
     * @param fieldExpr {@link java.lang.String}
     * @param newValue  {@link java.lang.Object}
     *
     * @return {@link IrDo}
     */
    @Override
    public IrDo update(final String fieldExpr, final Object newValue) {
        /*
         * Existing the QrItem
         */
        final IrItem item = new IrItem(fieldExpr).value(newValue);
        this.itExist(this.raw, item, (qr, ref) -> ref.put(qr.qrKey(), item.value()));
        return this;
    }

    @Override
    public JsonObject toJson() {
        return this.raw;
    }

    /**
     * Combine two `QrItem` based on different operator`, current version
     *
     * 1. = combine
     * 2. in combine
     *
     * Other operator could not be support
     *
     * @param raw     {@link io.vertx.core.json.JsonObject} The input json object
     * @param newItem {@link IrItem} the new added item to current object.
     * @param oldItem {@link IrItem} the original added item to current object.
     */
    @SuppressWarnings("all")
    private void saveWhere(final JsonObject raw, final IrItem newItem, final IrItem oldItem) {
        if (newItem.valueEq(oldItem)) {
            /* value equal, no change detect, skip */
            return;
        }
        /*
         * Here the qrKey is the same between newItem and oldItem
         */
        final Boolean isAnd = raw.getBoolean(VString.EMPTY, Boolean.FALSE);
        if (Ir.Op.EQ.equals(newItem.op())) {        // =
            // field,= or field
            raw.remove(newItem.qrKey());
            raw.remove(newItem.field());

            final JsonArray in = new JsonArray();
            // A = x OR A = y -> A in [x,y]
            // A = x AND A = y -> A in []
            if (!isAnd) {
                in.add(newItem.value());
                in.add(oldItem.value());
            }
            raw.put(newItem.field() + ",i", in);
        } else if (Ir.Op.IN.equals(newItem.op())) {
            raw.put(newItem.qrKey(), IrDo.combine(newItem.value(), oldItem.value(), isAnd));
        }
    }

    /**
     * Add new `QrItem` to current json criteria object.
     *
     * @param raw  {@link io.vertx.core.json.JsonObject} The input json object
     * @param item {@link IrItem} the new added item to current object.
     */
    private void addWhere(final JsonObject raw, final IrItem item) {
        if (HUt.isNil(raw)) {
            /*
             * Empty add new key directly here, because there is no condition,
             * in this kind of situation, the system will add `qrKey = value` to current
             * json object as the unique condition.
             */
            raw.put(item.qrKey(), item.value());
        } else {
            if (VValue.ONE == raw.size()) {
                /*
                 * If raw size = 1, add the condition to let the size to be 3.
                 * Here the connector will be `AND` auto.
                 */
                raw.put(VString.EMPTY, Boolean.TRUE);
                raw.put(item.qrKey(), item.value());
            } else {
                /*
                 * The raw size > 1, check connector
                 * 1. If And, add directly.
                 * 2. If Or, Convert the whole or condition to get the new one.
                 */
                final Boolean isAnd = raw.getBoolean(VString.EMPTY, Boolean.FALSE);
                if (isAnd) {
                    raw.put(item.qrKey(), item.value());
                } else {
                    /*
                     * Or connector
                     */
                    final JsonObject replaced = new JsonObject();
                    replaced.put(VString.EMPTY, Boolean.TRUE);
                    replaced.put(item.qrKey(), item.value());
                    replaced.put("$0", raw.copy());
                    /*
                     * Current raw will be replaced by new object.
                     */
                    raw.clear();
                    raw.mergeIn(replaced, true);
                }
            }
        }
    }

    private void itExist(final JsonObject source, final IrItem item,
                         final BiConsumer<IrItem, JsonObject> consumer) {
        this.itExist(source, (field, value) -> field.equals(item.qrKey()), consumer);
    }

    private void itExist(final JsonObject source,
                         final BiPredicate<String, Object> predicate,
                         final BiConsumer<IrItem, JsonObject> consumer) {
        if (isComplex(source)) {
            /* Complex criteria ( Tree Mode ) */
            source.copy().fieldNames().stream()
                .filter(field -> isJson(source.getValue(field)))
                .forEach(field -> {
                    final JsonObject itemJson = source.getJsonObject(field);
                    this.itExist(itemJson, predicate, consumer);
                    /*
                     * If the linear operation happened.
                     * Empty object processing
                     * Remove `{}` condition instead of other here.
                     * */
                    if (HUt.isNil(itemJson)) {
                        source.remove(field);
                    }
                });

        }
        this.itLinear(source, predicate, consumer);
    }

    private void itLinear(final JsonObject source,
                          final BiPredicate<String, Object> predicate,
                          final BiConsumer<IrItem, JsonObject> consumer) {
        /* Simple criteria ( Linear Mode ) */
        source.copy().fieldNames().stream()
            /* Non complex json */
            .filter(field -> !isJson(source.getValue(field)))
            .filter(field -> {
                // Predicate for `field = Object`
                final Object value = source.getValue(field);
                return predicate.test(field, value);
            })
            .forEach(field -> {
                // TiConsumer
                final Object value = source.getValue(field);
                final IrItem item = new IrItem(field).value(value);
                consumer.accept(item, source);
            });
        /*
         * Post operation: When only one key existing: `"" = xx`, remove it.
         *
         * Another situation is that such as:
         *
         * ```json
         * {
         *      "": true,
         *      "field1": "xxx"
         * }
         * ```
         *
         * This situation the `""` could not be removed because it will be calculated
         * in future ADD / APPEND etc.
         */
        if (VValue.ONE == source.size() && source.containsKey(VString.EMPTY)) {
            /* Removed single "" condition */
            source.remove(VString.EMPTY);
        }
    }
}