Add support comparable type constraints for scalar values
[sdc.git] / catalog-be / src / main / java / org / openecomp / sdc / be / datamodel / utils / PropertyValueConstraintValidationUtil.java
1 /*
2  * Copyright © 2016-2018 European Support Limited
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.openecomp.sdc.be.datamodel.utils;
17
18 import com.fasterxml.jackson.core.JsonProcessingException;
19 import com.fasterxml.jackson.core.type.TypeReference;
20 import com.fasterxml.jackson.databind.ObjectMapper;
21 import fj.data.Either;
22 import java.io.IOException;
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Objects;
30 import org.apache.commons.collections4.CollectionUtils;
31 import org.apache.commons.collections4.ListUtils;
32 import org.apache.commons.collections4.MapUtils;
33 import org.apache.commons.lang3.ArrayUtils;
34 import org.apache.commons.lang3.StringUtils;
35 import org.openecomp.sdc.be.components.impl.ResponseFormatManager;
36 import org.openecomp.sdc.be.dao.api.ActionStatus;
37 import org.openecomp.sdc.be.datatypes.elements.SchemaDefinition;
38 import org.openecomp.sdc.be.model.ComponentInstanceInput;
39 import org.openecomp.sdc.be.model.DataTypeDefinition;
40 import org.openecomp.sdc.be.model.InputDefinition;
41 import org.openecomp.sdc.be.model.PropertyConstraint;
42 import org.openecomp.sdc.be.model.PropertyDefinition;
43 import org.openecomp.sdc.be.model.cache.ApplicationDataTypeCache;
44 import org.openecomp.sdc.be.model.tosca.ToscaType;
45 import org.openecomp.sdc.be.model.tosca.constraints.ConstraintUtil;
46 import org.openecomp.sdc.be.model.tosca.constraints.LengthConstraint;
47 import org.openecomp.sdc.be.model.tosca.constraints.MaxLengthConstraint;
48 import org.openecomp.sdc.be.model.tosca.constraints.MinLengthConstraint;
49 import org.openecomp.sdc.be.model.tosca.constraints.exception.ConstraintValueDoNotMatchPropertyTypeException;
50 import org.openecomp.sdc.be.model.tosca.constraints.exception.ConstraintViolationException;
51 import org.openecomp.sdc.exception.ResponseFormat;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 public class PropertyValueConstraintValidationUtil {
56
57     private static final String UNDERSCORE = "_";
58     private static final String VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY = "%nValue provided in invalid format for %s property";
59     private static final Logger logger = LoggerFactory.getLogger(PropertyValueConstraintValidationUtil.class);
60     private static final String IGNORE_PROPERTY_VALUE_START_WITH = "{\"get_input\":";
61     private Map<String, DataTypeDefinition> dataTypeDefinitionCache;
62     private final ObjectMapper objectMapper = new ObjectMapper();
63     private final List<String> errorMessages = new ArrayList<>();
64     private StringBuilder completePropertyName;
65     private String completeInputName;
66
67     public Either<Boolean, ResponseFormat> validatePropertyConstraints(final Collection<? extends PropertyDefinition> propertyDefinitionList,
68                                                                        final ApplicationDataTypeCache applicationDataTypeCache,
69                                                                        final String model) {
70
71         dataTypeDefinitionCache = applicationDataTypeCache.getAll(model).left().value();
72         CollectionUtils.emptyIfNull(propertyDefinitionList).stream()
73             .filter(this::isValuePresent)
74             .forEach(this::evaluatePropertyTypeForConstraintValidation);
75         if (CollectionUtils.isNotEmpty(errorMessages)) {
76             final String errorMsgAsString = String.join(",", errorMessages);
77             logger.debug("Properties with Invalid Data: {}", errorMsgAsString);
78             return Either.right(getResponseFormatManager().getResponseFormat(ActionStatus.INVALID_PROPERTY_VALUES, errorMsgAsString));
79         }
80         return Either.left(Boolean.TRUE);
81     }
82
83     private boolean isValuePresent(PropertyDefinition propertyDefinition) {
84         if (propertyDefinition instanceof ComponentInstanceInput) {
85             return StringUtils.isNotEmpty(propertyDefinition.getValue());
86         }
87         if (propertyDefinition instanceof InputDefinition) {
88             return StringUtils.isNotEmpty(propertyDefinition.getDefaultValue());
89         }
90         return StringUtils.isNotEmpty(propertyDefinition.getValue());
91     }
92
93     private void evaluatePropertyTypeForConstraintValidation(PropertyDefinition propertyDefinition) {
94         if (propertyDefinition == null || propertyDefinition.getType() == null || !dataTypeDefinitionCache.containsKey(propertyDefinition.getType())) {
95             errorMessages.add("\nUnsupported datatype found for property " + getCompletePropertyName(propertyDefinition));
96             return;
97         }
98         completeInputName = "";
99         completePropertyName = new StringBuilder();
100         if (propertyDefinition instanceof ComponentInstanceInput) {
101             setCompletePropertyName(propertyDefinition);
102             evaluateComplexTypeProperties(propertyDefinition);
103             return;
104         }
105         if (propertyDefinition instanceof InputDefinition) {
106             completeInputName = propertyDefinition.getName();
107             propertyDefinition = getPropertyDefinitionObjectFromInputs(propertyDefinition);
108         }
109         if (propertyDefinition != null) {
110             List<PropertyConstraint> propertyConstraints =
111                 dataTypeDefinitionCache.get(propertyDefinition.getType()).safeGetConstraints();
112             if (ToscaType.isPrimitiveType(propertyDefinition.getType())) {
113                 propertyDefinition.setConstraints(org.openecomp.sdc.be.dao.utils.CollectionUtils.merge(propertyDefinition.safeGetConstraints(),
114                     propertyConstraints.isEmpty() ? new ArrayList<>() : propertyConstraints));
115                 evaluateConstraintsOnProperty(propertyDefinition);
116             } else if (ToscaType.isCollectionType(propertyDefinition.getType())) {
117                 propertyDefinition.setConstraints(org.openecomp.sdc.be.dao.utils.CollectionUtils.merge(propertyDefinition.safeGetConstraints(),
118                     propertyConstraints.isEmpty() ? new ArrayList<>() : propertyConstraints));
119                 evaluateConstraintsOnProperty(propertyDefinition);
120                 evaluateCollectionTypeProperties(propertyDefinition);
121             } else {
122                 setCompletePropertyName(propertyDefinition);
123                 evaluateComplexTypeProperties(propertyDefinition);
124             }
125         }
126     }
127
128     private void setCompletePropertyName(PropertyDefinition propertyDefinition) {
129         if (StringUtils.isNotBlank(propertyDefinition.getUniqueId())) {
130             completePropertyName.append(propertyDefinition.getUniqueId().substring(propertyDefinition.getUniqueId().lastIndexOf('.') + 1));
131         }
132     }
133
134     private void evaluateConstraintsOnProperty(PropertyDefinition propertyDefinition) {
135         ToscaType toscaType = ToscaType.isValidType(propertyDefinition.getType());
136         if (isPropertyNotMappedAsInput(propertyDefinition) && CollectionUtils.isNotEmpty(propertyDefinition.getConstraints())) {
137             for (PropertyConstraint propertyConstraint : propertyDefinition.getConstraints()) {
138                 try {
139                     propertyConstraint.initialize(toscaType);
140                     propertyConstraint.validate(toscaType, propertyDefinition.getValue());
141                 } catch (ConstraintValueDoNotMatchPropertyTypeException | ConstraintViolationException exception) {
142                     errorMessages.add(propertyConstraint.getErrorMessage(toscaType, exception, getCompletePropertyName(propertyDefinition)));
143                 }
144             }
145         } else if (isPropertyNotMappedAsInput(propertyDefinition) && ToscaType.isPrimitiveType(propertyDefinition.getType()) && !toscaType
146             .isValidValue(propertyDefinition.getValue())) {
147             errorMessages.add(String
148                 .format("\nUnsupported value provided for %s property supported value " + "type is %s.", getCompletePropertyName(propertyDefinition),
149                     toscaType.getType()));
150         }
151     }
152
153     private boolean isPropertyNotMappedAsInput(PropertyDefinition propertyDefinition) {
154         return !propertyDefinition.getValue().startsWith(IGNORE_PROPERTY_VALUE_START_WITH);
155     }
156
157     private void checkAndEvaluatePrimitiveProperty(PropertyDefinition propertyDefinition, DataTypeDefinition dataTypeDefinition) {
158         if (ToscaType.isPrimitiveType(dataTypeDefinition.getName()) && CollectionUtils.isNotEmpty(dataTypeDefinition.getConstraints())) {
159             PropertyDefinition definition = new PropertyDefinition();
160             definition.setValue(propertyDefinition.getValue());
161             definition.setType(dataTypeDefinition.getName());
162             definition.setConstraints(dataTypeDefinition.getConstraints());
163             evaluateConstraintsOnProperty(propertyDefinition);
164         }
165     }
166
167     private void evaluateComplexTypeProperties(PropertyDefinition propertyDefinition) {
168         List<PropertyDefinition> propertyDefinitions = dataTypeDefinitionCache.get(propertyDefinition.getType()).getProperties();
169         try {
170             Map<String, Object> valueMap = MapUtils
171                 .emptyIfNull(ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {
172                 }));
173             if (CollectionUtils.isEmpty(propertyDefinitions)) {
174                 checkAndEvaluatePrimitiveProperty(propertyDefinition, dataTypeDefinitionCache.get(propertyDefinition.getType()));
175             } else {
176                 ListUtils.emptyIfNull(propertyDefinitions).forEach(prop -> evaluateRegularComplexType(propertyDefinition, prop, valueMap));
177             }
178         } catch (ConstraintValueDoNotMatchPropertyTypeException e) {
179             logger.debug(e.getMessage(), e);
180             errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, getCompletePropertyName(propertyDefinition)));
181         }
182     }
183
184     private void evaluateRegularComplexType(PropertyDefinition propertyDefinition, PropertyDefinition prop, Map<String, Object> valueMap) {
185         try {
186             PropertyDefinition newPropertyWithValue;
187             if (valueMap.containsKey(prop.getName()) ) {
188                 if (ToscaType.isPrimitiveType(prop.getType())) {
189                     newPropertyWithValue = copyPropertyWithNewValue(prop, String.valueOf(valueMap.get(prop.getName())));
190                     if (isPropertyToEvaluate(newPropertyWithValue)) {
191                         evaluateConstraintsOnProperty(newPropertyWithValue);
192                     }
193                 } else if (ToscaType.isCollectionType(prop.getType())) {
194                     newPropertyWithValue =
195                         copyPropertyWithNewValue(prop,
196                             objectMapper.writeValueAsString(valueMap.get(prop.getName())));
197                     if (isPropertyToEvaluate(newPropertyWithValue)) {
198                         evaluateCollectionTypeProperties(newPropertyWithValue);
199                     }
200                 } else {
201                     newPropertyWithValue =
202                         copyPropertyWithNewValue(prop,
203                             objectMapper.writeValueAsString(valueMap.get(prop.getName())));
204                     if (isPropertyToEvaluate(newPropertyWithValue)) {
205                         evaluateComplexTypeProperties(newPropertyWithValue);
206                     }
207                 }
208             }
209         } catch (IOException | ConstraintValueDoNotMatchPropertyTypeException e) {
210             logger.error(e.getMessage(), e);
211             errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, getCompletePropertyName(propertyDefinition)));
212         }
213     }
214
215     private boolean isPropertyToEvaluate(PropertyDefinition propertyDefinition) throws ConstraintValueDoNotMatchPropertyTypeException {
216         if (Boolean.FALSE.equals(propertyDefinition.isRequired())) {
217             if (!ToscaType.isCollectionType(propertyDefinition.getType())) {
218                 return StringUtils.isNotEmpty(propertyDefinition.getValue()) &&
219                     !"null".equals(propertyDefinition.getValue());
220             } else if (ToscaType.LIST == ToscaType.isValidType(propertyDefinition.getType())) {
221                 Collection<Object> list = ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {});
222                 return CollectionUtils.isNotEmpty(list);
223             } else {
224                 Map<String, Object> valueMap = MapUtils
225                     .emptyIfNull(ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {
226                     }));
227                 return MapUtils.isNotEmpty(valueMap);
228             }
229         } else {
230             return true;
231         }
232     }
233
234     private void evaluateCollectionTypeProperties(PropertyDefinition propertyDefinition) {
235         ToscaType toscaPropertyType = ToscaType.isValidType(propertyDefinition.getType());
236         try {
237             if (isPropertyToEvaluate(propertyDefinition)) {
238                 evaluateCollectionConstraints(propertyDefinition, toscaPropertyType);
239             }
240         } catch (ConstraintValueDoNotMatchPropertyTypeException e) {
241             logger.error(e.getMessage(), e);
242             errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, getCompletePropertyName(propertyDefinition)));
243         }
244         if (ToscaType.LIST == toscaPropertyType) {
245             evaluateListType(propertyDefinition);
246         } else if (ToscaType.MAP == toscaPropertyType) {
247             evaluateMapType(propertyDefinition);
248         }
249     }
250
251     private void evaluateCollectionConstraints(PropertyDefinition propertyDefinition, ToscaType toscaPropertyType) {
252         List<PropertyConstraint> constraintsList = propertyDefinition.getConstraints();
253
254         if (CollectionUtils.isEmpty(constraintsList)) {
255             return;
256         }
257         ToscaType toscaPropertyType1;
258         if (null == toscaPropertyType) {
259             toscaPropertyType1 = ToscaType.isValidType(propertyDefinition.getType());
260         } else {
261             toscaPropertyType1 = toscaPropertyType;
262         }
263         constraintsList.stream()
264             .filter(this::isACollectionConstraint)
265             .forEach(propertyConstraint -> {
266                 try {
267                     if (ToscaType.LIST == toscaPropertyType1) {
268                         Collection<Object> list = ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {});
269                         propertyConstraint.validate(list);
270                     } else if (ToscaType.MAP == toscaPropertyType1) {
271                         final Map<String, Object> map = ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {});
272                         propertyConstraint.validate(map);
273                     }
274                 } catch (ConstraintValueDoNotMatchPropertyTypeException | ConstraintViolationException exception) {
275                     errorMessages.add("\n" + propertyConstraint.getErrorMessage(toscaPropertyType1, exception,
276                         getCompletePropertyName(propertyDefinition)));
277                 }
278             });
279     }
280
281     private boolean isACollectionConstraint(PropertyConstraint constraint) {
282         if (constraint instanceof MaxLengthConstraint){
283             return true;
284         }
285         if (constraint instanceof MinLengthConstraint) {
286             return true;
287         }
288         return constraint instanceof LengthConstraint;
289     }
290
291     private void evaluateListType(PropertyDefinition propertyDefinition) {
292         try {
293             if (propertyDefinition.getSchemaType() == null) {
294                 propertyDefinition.setSchema(createStringSchema());
295             }
296             Collection<Object> list = ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {});
297             evaluateCollectionType(propertyDefinition, list);
298         } catch (ConstraintValueDoNotMatchPropertyTypeException e) {
299             logger.debug(e.getMessage(), e);
300             errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, getCompletePropertyName(propertyDefinition)));
301         }
302     }
303
304     private SchemaDefinition createStringSchema() {
305         final SchemaDefinition schemaDefinition = new SchemaDefinition();
306         final PropertyDefinition schemaStringProperty = new PropertyDefinition();
307         schemaStringProperty.setType(ToscaType.STRING.getType());
308         schemaDefinition.setProperty(schemaStringProperty);
309         return schemaDefinition;
310     }
311
312     private void evaluateMapType(final PropertyDefinition propertyDefinition) {
313         try {
314             if (propertyDefinition.getSchemaType() == null) {
315                 propertyDefinition.setSchema(createStringSchema());
316             }
317             final Map<String, Object> map = ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {});
318             evaluateCollectionType(propertyDefinition, map.values());
319         } catch (ConstraintValueDoNotMatchPropertyTypeException e) {
320             logger.debug(e.getMessage(), e);
321             errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, getCompletePropertyName(propertyDefinition)));
322         }
323     }
324
325     private void evaluateCollectionPrimitiveSchemaType(final PropertyDefinition propertyDefinition,
326                                                        final String schemaType) throws JsonProcessingException {
327         if (propertyDefinition.getSchema() != null && propertyDefinition.getSchema().getProperty() instanceof PropertyDefinition) {
328             propertyDefinition.setConstraints(((PropertyDefinition) propertyDefinition.getSchema().getProperty()).getConstraints());
329             propertyDefinition.setValue(objectMapper.readValue(propertyDefinition.getValue(), String.class));
330             propertyDefinition.setType(schemaType);
331             evaluateConstraintsOnProperty(propertyDefinition);
332         }
333     }
334
335     private void evaluateCollectionType(final PropertyDefinition propertyDefinition, final Collection<Object> valueList) {
336         final String schemaType = propertyDefinition.getSchemaType();
337         for (final Object value : valueList) {
338             try {
339                 final PropertyDefinition propertyCopyWithNewValue = copyPropertyWithNewValue(propertyDefinition, objectMapper.writeValueAsString(value));
340                 if (ToscaType.isPrimitiveType(schemaType)) {
341                     evaluateCollectionPrimitiveSchemaType(propertyCopyWithNewValue, schemaType);
342                 } else if (ToscaType.isCollectionType(schemaType)) {
343                     propertyCopyWithNewValue.setType(schemaType);
344                     propertyCopyWithNewValue.setSchemaType(propertyDefinition.getSchemaProperty().getSchemaType());
345                     evaluateCollectionTypeProperties(propertyCopyWithNewValue);
346                 } else {
347                     propertyCopyWithNewValue.setType(schemaType);
348                     completePropertyName.append(UNDERSCORE);
349                     completePropertyName.append(propertyCopyWithNewValue.getName());
350                     evaluateComplexTypeProperties(propertyCopyWithNewValue);
351                 }
352             } catch (final Exception e) {
353                 logger.debug(e.getMessage(), e);
354                 errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, getCompletePropertyName(propertyDefinition)));
355             }
356         }
357     }
358
359     private String getCompletePropertyName(final PropertyDefinition propertyDefinition) {
360         if (StringUtils.isNotBlank(completeInputName)) {
361             return completeInputName;
362         }
363
364         final String propertyName = propertyDefinition == null ? "" : propertyDefinition.getName();
365         if (StringUtils.isNotBlank(completePropertyName)) {
366             return completePropertyName + UNDERSCORE + propertyName;
367         }
368
369         return propertyName;
370     }
371
372     private PropertyDefinition copyPropertyWithNewValue(final PropertyDefinition propertyToCopy, final String value) {
373         final var propertyDefinition = new PropertyDefinition(propertyToCopy);
374         propertyDefinition.setValue(value);
375         return propertyDefinition;
376     }
377
378     private PropertyDefinition getPropertyDefinitionObjectFromInputs(PropertyDefinition property) {
379         InputDefinition inputDefinition = (InputDefinition) property;
380         PropertyDefinition propertyDefinition = null;
381         if (CollectionUtils.isEmpty(inputDefinition.getProperties()) || ToscaType.isPrimitiveType(inputDefinition.getProperties().get(0).getType())) {
382             propertyDefinition = new PropertyDefinition();
383             propertyDefinition.setType(inputDefinition.getType());
384             propertyDefinition.setValue(inputDefinition.getDefaultValue());
385             propertyDefinition.setName(inputDefinition.getName());
386         } else if (Objects.nonNull(inputDefinition.getInputPath())) {
387             propertyDefinition = evaluateComplexTypeInputs(inputDefinition);
388         }
389         return propertyDefinition;
390     }
391
392     private PropertyDefinition evaluateComplexTypeInputs(InputDefinition inputDefinition) {
393         Map<String, Object> inputMap = new HashMap<>();
394         PropertyDefinition propertyDefinition = new PropertyDefinition();
395         String[] inputPathArr = inputDefinition.getInputPath().split("#");
396         if (inputPathArr.length > 1) {
397             inputPathArr = ArrayUtils.remove(inputPathArr, 0);
398         }
399         try {
400             Map<String, Object> presentMap = inputMap;
401             for (int i = 0; i < inputPathArr.length; i++) {
402                 if (i == inputPathArr.length - 1) {
403                     presentMap.computeIfAbsent(inputPathArr[i], k -> inputDefinition.getDefaultValue());
404                 } else {
405                     presentMap.computeIfAbsent(inputPathArr[i], k -> new HashMap<String, Object>());
406                     presentMap = (Map<String, Object>) presentMap.get(inputPathArr[i]);
407                 }
408             }
409             if (CollectionUtils.isNotEmpty(inputDefinition.getProperties())) {
410                 propertyDefinition.setType(inputDefinition.getProperties().get(0).getType());
411             }
412             propertyDefinition.setName(inputDefinition.getName());
413             propertyDefinition.setValue(objectMapper.writeValueAsString(inputMap));
414         } catch (IOException e) {
415             logger.error(e.getMessage(), e);
416             errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, inputDefinition.getName()));
417         }
418         return propertyDefinition;
419     }
420
421     protected ResponseFormatManager getResponseFormatManager() {
422         return ResponseFormatManager.getInstance();
423     }
424 }