# ArrayValidator.java

    /*
     * Copyright (c) 2019, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
     *
     * WSO2 Inc. licenses this file to you 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
     *
     * http://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 org.apache.synapse.commons.json.jsonprocessor.validators;

    import com.google.gson.JsonArray;
    import com.google.gson.JsonElement;
    import com.google.gson.JsonNull;
    import com.google.gson.JsonObject;
    import com.google.gson.JsonParser;
    import org.apache.synapse.commons.json.jsonprocessor.constants.ValidatorConstants;
    import org.apache.synapse.commons.json.jsonprocessor.exceptions.ParserException;
    import org.apache.synapse.commons.json.jsonprocessor.exceptions.ValidatorException;
    import org.apache.synapse.commons.json.jsonprocessor.utils.DataTypeConverter;
    import org.apache.synapse.commons.json.jsonprocessor.utils.GSONDataTypeConverter;
    import org.apache.synapse.commons.json.jsonprocessor.utils.JsonProcessorUtils;

    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;

    /**
     * This class will validate json arrays according to the schema.
     */
    public class ArrayValidator {

        private static final String MIN_ITEMS = "minItems";
        private static final String MAX_ITEMS = "maxItems";
        private static final String ITEMS = "items";
        private static final String UNIQUE_ITEMS = "uniqueItems";
        private static final String ADDITIONAL_ITEMS = "additionalItems";

        // Use without instantiation
        private ArrayValidator() {

        }

        /**
         * This method will validates an input array according to a given schema.
         *
         * @param input  input array as a Map.
         * @param schema JSON schema as an object.
         * @return Validated JSON array.
         * @throws ValidatorException Exception occurs in validation process.
         * @throws ParserException    Exception occurs in data type parsing.
         */
        public static JsonArray validateArray(Map.Entry<String, JsonElement> input, JsonObject
                schema) throws ValidatorException, ParserException {

            JsonParser parser;
            int minItems = -1;
            int maxItems = -1;
            boolean uniqueItems = false;
            boolean notAllowAdditional = false;
            // parsing the properties related to arrays from the schema, if they exists.
            if (schema.has(UNIQUE_ITEMS)) {
                String uniqueItemsString = JsonProcessorUtils.replaceEnclosingQuotes(
                        schema.get(UNIQUE_ITEMS).getAsString());
                if (!uniqueItemsString.isEmpty()) {
                    uniqueItems = DataTypeConverter.convertToBoolean(uniqueItemsString);
                }
            }
            if (schema.has(MIN_ITEMS)) {
                String minItemsString = JsonProcessorUtils.replaceEnclosingQuotes(schema.get(MIN_ITEMS).getAsString());
                if (!minItemsString.isEmpty()) {
                    minItems = DataTypeConverter.convertToInt(minItemsString);
                    if (minItems < 0) {
                        throw new ValidatorException("Invalid minItems constraint in the schema");
                    }
                }
            }
            if (schema.has(MAX_ITEMS)) {
                String maxItemsString = JsonProcessorUtils.replaceEnclosingQuotes(schema.get(MAX_ITEMS).getAsString());
                if (!maxItemsString.isEmpty()) {
                    maxItems = DataTypeConverter.convertToInt(maxItemsString);
                    if (maxItems < 0) {
                        throw new ValidatorException("Invalid maxItems constraint in the schema");
                    }
                }
            }

            // parsing additionalItems
            // We are wrapping the additionalItems schema inside a json array schema object and calling this same method
            // again
            JsonObject additionalItemsSchema = null;
            if (schema.has(ADDITIONAL_ITEMS)) {
                JsonElement tempElement = schema.get(ADDITIONAL_ITEMS);
                if (tempElement.isJsonPrimitive() && !tempElement.getAsBoolean()) {
                    notAllowAdditional = true;
                } else if (tempElement.isJsonObject() && !tempElement.getAsJsonObject().entrySet().isEmpty()) {
                    StringBuffer jsonString = new StringBuffer("{\"type\": \"array\",\"items\": ");
                    jsonString.append(schema.get(ADDITIONAL_ITEMS).toString());
                    jsonString.append("}");
                    parser = new JsonParser();
                    additionalItemsSchema = parser.parse(jsonString.toString()).getAsJsonObject();
                }
            }

            // Convert the input to an array. If possible, do the single element array correction. Ex 45 -> [45]
            // else throw an error
            JsonArray inputArray;
            if (input.getValue().isJsonArray()) {
                inputArray = input.getValue().getAsJsonArray();
            } else {
                inputArray = singleElementArrayCorrection(input.getValue());
            }

            // Structural validations
            doStructuralValidations(inputArray, minItems, maxItems, uniqueItems);

            // processing the items property in JSON array.
            if (schema.has(ITEMS)) {
                // Items must be either a valid JSON Schema or an array of valid JSON Schemas.
                if (schema.get(ITEMS).isJsonArray()) {
                    // Items - valid JSON array.
                    JsonArray schemaArray = schema.get(ITEMS).getAsJsonArray();
                    processSchemaWithItemsArray(inputArray, schemaArray, additionalItemsSchema, notAllowAdditional);
                    // take all instances from json array and iteratively validate them.
                } else if (schema.get(ITEMS).isJsonObject()) {
                    // Item is a JSON object
                    JsonObject schemaObject = schema.get(ITEMS).getAsJsonObject();
                    processSchemaWithOneItem(inputArray, schemaObject);
                } else {
                    throw new ValidatorException("Schema for Array is invalid. " +
                            "Should contain either JsonArray or JsonObject");
                }
            }
            return inputArray;
        }

        /**
         * JSON structure correction. Convert single elements to arrays.
         *
         * @param element JsonElement payload.
         * @return Json array.
         */
        private static JsonArray singleElementArrayCorrection(JsonElement element) {

            JsonArray array = new JsonArray();
            array.add(element);
            return array;
        }

        /**
         * Validate JSON array when both items and schema are arrays.
         * Ex:- {"type":"array", "items":[{"type": "boolean"},{"type": "numeric"}]}
         *
         * @param inputArray  input data as json array.
         * @param schemaArray inout schema as json array.
         * @throws ValidatorException validation exception occurs.
         * @throws ParserException    parsing exception occurs.
         */
        private static void processSchemaWithItemsArray(JsonArray inputArray, JsonArray schemaArray, JsonObject
                additionalItemsSchema, boolean notAllowAdditional) throws ValidatorException, ParserException {

            if (notAllowAdditional && inputArray.size() > schemaArray.size()) {
                throw new ValidatorException(
                        "Array : " + inputArray.toString() + " has more items than allowed in the schema");
            }
            int i = 0;
            for (JsonElement element : schemaArray) {
                JsonObject tempObj = element.getAsJsonObject();
                // Checking for empty input schema Ex:- {}
                if (!tempObj.entrySet().isEmpty()) {
                    if (tempObj.has(ValidatorConstants.TYPE_KEY)) {
                        String type = JsonProcessorUtils.replaceEnclosingQuotes(tempObj.get(ValidatorConstants.TYPE_KEY).toString());
                        if (ValidatorConstants.BOOLEAN_KEYS.contains(type)) {
                            inputArray.set(i, BooleanValidator.validateBoolean(tempObj, inputArray.get(i).getAsString()));
                        } else if (ValidatorConstants.NOMINAL_KEYS.contains(type)) {
                            inputArray.set(i, StringValidator.validateNominal(tempObj, inputArray.get(i).getAsString()));
                        } else if (ValidatorConstants.NUMERIC_KEYS.contains(type)) {
                            inputArray.set(i, NumericValidator.validateNumeric(tempObj, inputArray.get(i).getAsString()));
                        } else if (ValidatorConstants.ARRAY_KEYS.contains(type)) {
                            inputArray.set(i, ArrayValidator.validateArray(
                                    GSONDataTypeConverter.getMapFromString(inputArray.get(i).toString()), tempObj));
                        } else if (ValidatorConstants.NULL_KEYS.contains(type)) {
                            if (inputArray.get(i) != null) {
                                NullValidator.validateNull(tempObj, inputArray.get(i).toString());
                            }
                            inputArray.set(i, JsonNull.INSTANCE);
                        } else if (ValidatorConstants.OBJECT_KEYS.contains(type)) {
                            inputArray.set(i, ObjectValidator.validateObject(inputArray.get(i).getAsJsonObject(), tempObj));
                        }
                    } else {
                        throw new ValidatorException("Array items should contain a type " +
                                "declaration");
                    }
                }
                i++;
            }
            // additional schema validating the reset of the array
            if (additionalItemsSchema != null && inputArray.size() > schemaArray.size()) {
                JsonArray extraArray = new JsonArray();
                for (int j = i; j < inputArray.size(); j++) {
                    extraArray.add(inputArray.get(j));
                }
                extraArray = ArrayValidator.validateArray(GSONDataTypeConverter.getMapFromJsonArray(extraArray),
                        additionalItemsSchema);
                // putting back the values again after validation
                for (int j = 0; j < extraArray.size(); j++) {
                    inputArray.set(i + j, extraArray.get(j));
                }
            }
        }

        /**
         * Validate JSON array when items is a single JSON object.
         * Ex:- {"type":"array", "items":{"type": "boolean"}}
         *
         * @param inputArray   input data as json array.
         * @param schemaObject input schema as json object.
         * @throws ValidatorException validation exception occurs.
         * @throws ParserException    parsing exception occurs.
         */
        private static void processSchemaWithOneItem(JsonArray inputArray, JsonObject schemaObject) throws
                ValidatorException, ParserException {
            if (schemaObject.has(ValidatorConstants.TYPE_KEY)) {
                String type = JsonProcessorUtils.replaceEnclosingQuotes(
                        schemaObject.get(ValidatorConstants.TYPE_KEY).toString());
                int i = 0;
                if (ValidatorConstants.BOOLEAN_KEYS.contains(type)) {
                    for (JsonElement element : inputArray) {
                        inputArray.set(i, BooleanValidator.validateBoolean(schemaObject, element.getAsString()));
                        i++;
                    }
                } else if (ValidatorConstants.NUMERIC_KEYS.contains(type)) {
                    for (JsonElement element : inputArray) {
                        inputArray.set(i, NumericValidator.validateNumeric(schemaObject, element.getAsString()));
                        i++;
                    }
                } else if (ValidatorConstants.NOMINAL_KEYS.contains(type)) {
                    for (JsonElement element : inputArray) {
                        inputArray.set(i, StringValidator.validateNominal(schemaObject, element.getAsString()));
                        i++;
                    }
                } else if (ValidatorConstants.ARRAY_KEYS.contains(type)) {
                    for (JsonElement element : inputArray) {
                        inputArray.set(i, ArrayValidator.validateArray(GSONDataTypeConverter.getMapFromString(
                                element.getAsString()), schemaObject));
                        i++;
                    }
                } else if (ValidatorConstants.OBJECT_KEYS.contains(type)) {
                    for (JsonElement element : inputArray) {
                        inputArray.set(i, ObjectValidator.validateObject(element.getAsJsonObject(), schemaObject));
                        i++;
                    }
                } else if (ValidatorConstants.NULL_KEYS.contains(type)) {
                    for (JsonElement element : inputArray) {
                        if (element != null) {
                            NullValidator.validateNull(schemaObject, element.toString());
                        }
                        inputArray.set(i, JsonNull.INSTANCE);
                        i++;
                    }
                } else {
                    throw new ValidatorException("Schema for array must have a type declaration" +
                            schemaObject.toString());
                }
            }
        }

        /**
         * This method validates the structure of given JSON array against constraints.
         *
         * @param inputArray  input JSON array.
         * @param minItems    minimum items allowed.
         * @param maxItems    maximum items allowed.
         * @param uniqueItems array items should be unique.
         * @throws ValidatorException validation exception occurs.
         */
        private static void doStructuralValidations(JsonArray inputArray, int minItems, int maxItems, boolean
                uniqueItems) throws ValidatorException {

            final String errorMsg = "Error occurs while validating the structure of array : ";
            if (minItems != -1 && inputArray.size() < minItems) {
                throw new ValidatorException(errorMsg + inputArray.toString() +
                        ". Array violated the minItems constraint");
            }
            if (maxItems != -1 && inputArray.size() > maxItems) {
                throw new ValidatorException(errorMsg + inputArray.toString() +
                        ". Array violated the maxItems constraint");
            }
            if (uniqueItems) {
                Set<JsonElement> temporarySet = new HashSet<>();
                for (JsonElement element : inputArray) {
                    if (!temporarySet.add(element)) {
                        throw new ValidatorException(errorMsg +
                                inputArray.toString() + ". Array violated the uniqueItems constraint");
                    }
                }
            }
        }
    }