package alien4cloud.utils.services;
import static alien4cloud.utils.AlienUtils.safe;
import java.beans.IntrospectionException;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.alien4cloud.tosca.model.definitions.PropertyConstraint;
import org.alien4cloud.tosca.model.definitions.PropertyDefinition;
import org.alien4cloud.tosca.model.definitions.PropertyValue;
import org.alien4cloud.tosca.model.types.DataType;
import org.alien4cloud.tosca.model.types.PrimitiveDataType;
import org.springframework.stereotype.Service;
import alien4cloud.exception.InvalidArgumentException;
import alien4cloud.tosca.context.ToscaContext;
import alien4cloud.tosca.normative.IPropertyType;
import alien4cloud.tosca.normative.ToscaType;
import alien4cloud.tosca.properties.constraints.ConstraintUtil;
import alien4cloud.tosca.properties.constraints.ConstraintUtil.ConstraintInformation;
import alien4cloud.tosca.properties.constraints.exception.ConstraintTechnicalException;
import alien4cloud.tosca.properties.constraints.exception.ConstraintValueDoNotMatchPropertyTypeException;
import alien4cloud.tosca.properties.constraints.exception.ConstraintViolationException;
import alien4cloud.utils.VersionUtil;
import alien4cloud.utils.version.InvalidVersionException;
import lombok.extern.slf4j.Slf4j;
/**
* Common property constraint utils
*/
@Slf4j
// FIXME: shouldn't be a service anymore since all its methods are static
@Service
public class ConstraintPropertyService {
/**
* Check the constraints on an unwrapped property value (basically a string, map or list).
*
* @param propertyName The name of the property.
* @param propertyValue The value of the property to check.
* @param propertyDefinition The property definition that defines the property to check.
* @throws ConstraintValueDoNotMatchPropertyTypeException In case the value type doesn't match the type of the property as defined.
* @throws ConstraintViolationException In case the value doesn't match one of the constraints defined on the property.
*/
public static void checkPropertyConstraint(String propertyName, Object propertyValue, PropertyDefinition propertyDefinition)
throws ConstraintValueDoNotMatchPropertyTypeException, ConstraintViolationException {
checkPropertyConstraint(propertyName, propertyValue, propertyDefinition, null);
}
/**
* Check the constraints on an unwrapped property value (basically a string, map or list) and get events through the given consumer parameter when missing
* properties on complex data type are found.
* Note that the property value cannot be null and the required characteristic of the initial property definition will NOT be checked.
*
* @param propertyName The name of the property.
* @param propertyValue The value of the property to check.
* @param propertyDefinition The property definition that defines the property to check.
* @param missingPropertyConsumer A consumer to receive events when a required property is not defined on a complex type sub-field.
* @throws ConstraintValueDoNotMatchPropertyTypeException In case the value type doesn't match the type of the property as defined.
* @throws ConstraintViolationException In case the value doesn't match one of the constraints defined on the property.
*/
public static void checkPropertyConstraint(String propertyName, Object propertyValue, PropertyDefinition propertyDefinition,
Consumer<String> missingPropertyConsumer) throws ConstraintValueDoNotMatchPropertyTypeException, ConstraintViolationException {
Object value = propertyValue;
if (propertyValue instanceof PropertyValue) {
value = ((PropertyValue) propertyValue).getValue();
}
boolean isPrimitiveType = false;
boolean isTypeDerivedFromPrimitive = false;
DataType dataType = null;
String typeName = propertyDefinition.getType();
if (ToscaType.isPrimitive(typeName)) {
isPrimitiveType = true;
} else {
dataType = ToscaContext.get(DataType.class, typeName);
if (dataType instanceof PrimitiveDataType) {
// the type is derived from a primitive type
isTypeDerivedFromPrimitive = true;
}
}
if (value instanceof String) {
if (isPrimitiveType) {
checkSimplePropertyConstraint(propertyName, (String) value, propertyDefinition);
} else if (isTypeDerivedFromPrimitive) {
checkComplexPropertyDerivedFromPrimitiveTypeConstraints(propertyName, (String) value, propertyDefinition, dataType);
} else {
throw new ConstraintValueDoNotMatchPropertyTypeException(
"Property value is a String while the expected data type is a complex type " + value.getClass().getName());
}
} else if (value instanceof Map) {
checkComplexPropertyConstraint(propertyName, (Map<String, Object>) value, propertyDefinition, missingPropertyConsumer);
} else if (value instanceof List) {
checkListPropertyConstraint(propertyName, (List<Object>) value, propertyDefinition, missingPropertyConsumer);
} else {
throw new InvalidArgumentException(
"Not expecting to receive constraint validation for other types than String, Map or List as " + value.getClass().getName());
}
}
/**
* Check constraints defined on a property for a specified value
*
* @param propertyName Property name (mainly used to create a comprehensive error message)
* @param stringValue Tested property value
* @param propertyDefinition Full property definition with type, constraints, default value,...
* @throws ConstraintViolationException
* @throws ConstraintValueDoNotMatchPropertyTypeException
*/
public static void checkSimplePropertyConstraint(final String propertyName, final String stringValue, final PropertyDefinition propertyDefinition)
throws ConstraintViolationException, ConstraintValueDoNotMatchPropertyTypeException {
ConstraintInformation consInformation = null;
// check any property definition without constraints (type/value)
checkBasicType(propertyName, propertyDefinition.getType(), stringValue);
if (propertyDefinition.getConstraints() != null && !propertyDefinition.getConstraints().isEmpty()) {
IPropertyType<?> toscaType = ToscaType.fromYamlTypeName(propertyDefinition.getType());
for (PropertyConstraint constraint : propertyDefinition.getConstraints()) {
try {
consInformation = ConstraintUtil.getConstraintInformation(constraint);
consInformation.setPath(propertyName + ".constraints[" + consInformation.getName() + "]");
constraint.initialize(toscaType);
constraint.validate(toscaType, stringValue);
} catch (ConstraintViolationException e) {
throw new ConstraintViolationException(e.getMessage(), e, consInformation);
} catch (IntrospectionException e) {
// ConstraintValueDoNotMatchPropertyTypeException is not supposed to be raised here (only in constraint definition validation)
log.info("Constraint introspection error for property <" + propertyName + "> value <" + stringValue + ">", e);
throw new ConstraintTechnicalException("Constraint introspection error for property <" + propertyName + "> value <" + stringValue + ">", e);
}
}
}
}
/**
* Check constraints defined on a property which has a type derived from a primitive.
*/
private static void checkComplexPropertyDerivedFromPrimitiveTypeConstraints(final String propertyName, final String stringValue,
final PropertyDefinition propertyDefinition, final DataType dataType)
throws ConstraintViolationException, ConstraintValueDoNotMatchPropertyTypeException {
ConstraintInformation consInformation = null;
boolean hasDefinitionConstraints = propertyDefinition.getConstraints() != null && !propertyDefinition.getConstraints().isEmpty();
boolean hasTypeConstraints = false;
if (dataType instanceof PrimitiveDataType && ((PrimitiveDataType) dataType).getConstraints() != null
&& !((PrimitiveDataType) dataType).getConstraints().isEmpty()) {
hasTypeConstraints = true;
}
String derivedFromPrimitiveType = dataType.getDerivedFrom().get(0);
// Check the type of the property even if there is no constraints.
checkBasicType(propertyName, derivedFromPrimitiveType, stringValue);
if (hasDefinitionConstraints || hasTypeConstraints) { // check the constraints if there is any defined
if (hasDefinitionConstraints) {
checkConstraints(propertyName, stringValue, derivedFromPrimitiveType, propertyDefinition.getConstraints());
}
if (hasTypeConstraints) {
checkConstraints(propertyName, stringValue, derivedFromPrimitiveType, ((PrimitiveDataType) dataType).getConstraints());
}
}
}
private static void checkConstraints(final String propertyName, final String stringValue, final String typeName, List<PropertyConstraint> constraints)
throws ConstraintViolationException, ConstraintValueDoNotMatchPropertyTypeException {
ConstraintInformation consInformation = null;
for (PropertyConstraint constraint : constraints) {
IPropertyType<?> toscaType = ToscaType.fromYamlTypeName(typeName);
try {
consInformation = ConstraintUtil.getConstraintInformation(constraint);
consInformation.setPath(propertyName + ".constraints[" + consInformation.getName() + "]");
constraint.initialize(toscaType);
constraint.validate(toscaType, stringValue);
} catch (ConstraintViolationException e) {
throw new ConstraintViolationException(e.getMessage(), e, consInformation);
} catch (IntrospectionException e) {
// ConstraintValueDoNotMatchPropertyTypeException is not supposed to be raised here (only in constraint definition validation)
log.info("Constraint introspection error for property <" + propertyName + "> value <" + stringValue + ">", e);
throw new ConstraintTechnicalException("Constraint introspection error for property <" + propertyName + "> value <" + stringValue + ">", e);
}
}
}
private static void checkDataTypePropertyConstraint(String propertyName, Map<String, Object> complexPropertyValue, PropertyDefinition propertyDefinition,
Consumer<String> missingPropertyConsumer) throws ConstraintViolationException, ConstraintValueDoNotMatchPropertyTypeException {
DataType dataType = ToscaContext.get(DataType.class, propertyDefinition.getType());
if (dataType == null) {
throw new ConstraintViolationException(
"Complex type " + propertyDefinition.getType() + " is not complex or it cannot be found in the archive nor in Alien");
}
for (Map.Entry<String, Object> complexPropertyValueEntry : complexPropertyValue.entrySet()) {
if (!safe(dataType.getProperties()).containsKey(complexPropertyValueEntry.getKey())) {
throw new ConstraintViolationException("Complex type " + propertyDefinition.getType() + " do not have nested property with name "
+ complexPropertyValueEntry.getKey() + " for property " + propertyName);
} else {
Object nestedPropertyValue = complexPropertyValueEntry.getValue();
PropertyDefinition nestedPropertyDefinition = dataType.getProperties().get(complexPropertyValueEntry.getKey());
checkPropertyConstraint(propertyName + "." + complexPropertyValueEntry.getKey(), nestedPropertyValue, nestedPropertyDefinition,
missingPropertyConsumer);
}
}
// check if the data type has required missing properties
if (missingPropertyConsumer != null) {
for (Map.Entry<String, PropertyDefinition> dataTypeDefinition : safe(dataType.getProperties()).entrySet()) {
if (dataTypeDefinition.getValue().isRequired() && !complexPropertyValue.containsKey(dataTypeDefinition.getKey())) {
// A required property is missing
String missingPropertyName = propertyName + "." + dataTypeDefinition.getKey();
missingPropertyConsumer.accept(missingPropertyName);
}
}
}
}
private static void checkListPropertyConstraint(String propertyName, List<Object> listPropertyValue, PropertyDefinition propertyDefinition,
Consumer<String> missingPropertyConsumer) throws ConstraintValueDoNotMatchPropertyTypeException, ConstraintViolationException {
PropertyDefinition entrySchema = propertyDefinition.getEntrySchema();
for (int i = 0; i < listPropertyValue.size(); i++) {
checkPropertyConstraint(propertyName + "[" + String.valueOf(i) + "]", listPropertyValue.get(i), entrySchema, missingPropertyConsumer);
}
}
private static void checkMapPropertyConstraint(String propertyName, Map<String, Object> complexPropertyValue, PropertyDefinition propertyDefinition,
Consumer<String> missingPropertyConsumer) throws ConstraintValueDoNotMatchPropertyTypeException, ConstraintViolationException {
PropertyDefinition entrySchema = propertyDefinition.getEntrySchema();
for (Map.Entry<String, Object> complexPropertyValueEntry : complexPropertyValue.entrySet()) {
checkPropertyConstraint(propertyName + "." + complexPropertyValueEntry.getKey(), complexPropertyValueEntry.getValue(), entrySchema,
missingPropertyConsumer);
}
}
/**
* Verify that a complex property value correspond to its definition of constraints
*
* @param propertyName name of the property
* @param complexPropertyValue the value
* @param propertyDefinition the definition
* @throws ConstraintViolationException
* @throws ConstraintValueDoNotMatchPropertyTypeException
*/
private static void checkComplexPropertyConstraint(String propertyName, Map<String, Object> complexPropertyValue, PropertyDefinition propertyDefinition,
Consumer<String> missingPropertyConsumer) throws ConstraintViolationException, ConstraintValueDoNotMatchPropertyTypeException {
if (ToscaType.MAP.equals(propertyDefinition.getType())) {
checkMapPropertyConstraint(propertyName, complexPropertyValue, propertyDefinition, missingPropertyConsumer);
} else {
checkDataTypePropertyConstraint(propertyName, complexPropertyValue, propertyDefinition, missingPropertyConsumer);
}
}
/**
* Check that a given value is matching the native type defined on the property definition.
*
* @param propertyName The name of the property under validation
* @param primitiveType The primitive type to check the value against.
* @param propertyValue The value to check.
* @throws ConstraintValueDoNotMatchPropertyTypeException in case the value does not match the primitive type.
*/
private static void checkBasicType(final String propertyName, final String primitiveType, final String propertyValue)
throws ConstraintValueDoNotMatchPropertyTypeException {
// check basic type value : "boolean" (not handled, no exception thrown)
// "string" (basic case, no exception), "float", "integer", "version"
try {
switch (primitiveType) {
case "integer":
Integer.parseInt(propertyValue);
break;
case "float":
Float.parseFloat(propertyValue);
break;
case "version":
VersionUtil.parseVersion(propertyValue);
break;
default:
// last type "string" can have any format
break;
}
} catch (NumberFormatException | InvalidVersionException e) {
log.debug("The property value for property {} is not of type {}: {}", propertyName, primitiveType, propertyValue, e);
ConstraintInformation consInformation = new ConstraintInformation(propertyName, null, propertyValue, primitiveType);
throw new ConstraintValueDoNotMatchPropertyTypeException(e.getMessage(), e, consInformation);
}
}
}