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");
}
}
}
}
}