Validate service input default values against constraints
[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.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Objects;
29 import org.apache.commons.collections4.CollectionUtils;
30 import org.apache.commons.collections4.ListUtils;
31 import org.apache.commons.collections4.MapUtils;
32 import org.apache.commons.lang3.ArrayUtils;
33 import org.apache.commons.lang3.StringUtils;
34 import org.openecomp.sdc.be.components.impl.ResponseFormatManager;
35 import org.openecomp.sdc.be.dao.api.ActionStatus;
36 import org.openecomp.sdc.be.datatypes.elements.SchemaDefinition;
37 import org.openecomp.sdc.be.datatypes.elements.SubPropertyToscaFunction;
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(
95             propertyDefinition.getType())) {
96             errorMessages.add("\nUnsupported datatype found for property " + getCompletePropertyName(propertyDefinition));
97             return;
98         }
99         completeInputName = "";
100         completePropertyName = new StringBuilder();
101         if (propertyDefinition instanceof ComponentInstanceInput) {
102             setCompletePropertyName(propertyDefinition);
103             evaluateComplexTypeProperties(propertyDefinition);
104             return;
105         }
106         if (propertyDefinition instanceof InputDefinition) {
107             completeInputName = propertyDefinition.getName();
108             propertyDefinition = getPropertyDefinitionObjectFromInputs(propertyDefinition);
109         }
110         if (propertyDefinition != null) {
111             List<PropertyConstraint> propertyConstraints =
112                 dataTypeDefinitionCache.get(propertyDefinition.getType()).safeGetConstraints();
113             if (ToscaType.isPrimitiveType(propertyDefinition.getType())) {
114                 propertyDefinition.setConstraints(org.openecomp.sdc.be.dao.utils.CollectionUtils.merge(propertyDefinition.safeGetConstraints(),
115                     propertyConstraints.isEmpty() ? new ArrayList<>() : propertyConstraints));
116                 evaluateConstraintsOnProperty(propertyDefinition);
117             } else if (ToscaType.isCollectionType(propertyDefinition.getType())) {
118                 propertyDefinition.setConstraints(org.openecomp.sdc.be.dao.utils.CollectionUtils.merge(propertyDefinition.safeGetConstraints(),
119                     propertyConstraints.isEmpty() ? new ArrayList<>() : propertyConstraints));
120                 evaluateConstraintsOnProperty(propertyDefinition);
121                 evaluateCollectionTypeProperties(propertyDefinition);
122             } else {
123                 setCompletePropertyName(propertyDefinition);
124                 evaluateComplexTypeProperties(propertyDefinition);
125             }
126         }
127     }
128
129     private void setCompletePropertyName(PropertyDefinition propertyDefinition) {
130         if (StringUtils.isNotBlank(propertyDefinition.getUniqueId())) {
131             completePropertyName.append(propertyDefinition.getUniqueId().substring(propertyDefinition.getUniqueId().lastIndexOf('.') + 1));
132         }
133     }
134
135     private void evaluateConstraintsOnProperty(PropertyDefinition propertyDefinition) {
136         ToscaType toscaType = ToscaType.isValidType(propertyDefinition.getType());
137         if (isPropertyNotMappedAsInput(propertyDefinition) && CollectionUtils.isNotEmpty(propertyDefinition.getConstraints())) {
138             for (PropertyConstraint propertyConstraint : propertyDefinition.getConstraints()) {
139                 try {
140                     propertyConstraint.initialize(toscaType);
141                     propertyConstraint.validate(toscaType, propertyDefinition.getValue());
142                 } catch (ConstraintValueDoNotMatchPropertyTypeException | ConstraintViolationException exception) {
143                     errorMessages.add(propertyConstraint.getErrorMessage(toscaType, exception, getCompletePropertyName(propertyDefinition)));
144                 } catch (IllegalArgumentException ie) {
145                     errorMessages.add(ie.getMessage());
146                 }
147             }
148         } else if (isPropertyNotMappedAsInput(propertyDefinition) && ToscaType.isPrimitiveType(propertyDefinition.getType())
149                 && !propertyDefinition.isToscaFunction() && !toscaType.isValidValue(propertyDefinition.getValue())) {
150             errorMessages.add(String.format("Unsupported value provided for %s property supported value type is %s.",
151                 getCompletePropertyName(propertyDefinition), toscaType.getType()));
152         }
153     }
154
155     private boolean isPropertyNotMappedAsInput(PropertyDefinition propertyDefinition) {
156         return !propertyDefinition.getValue().startsWith(IGNORE_PROPERTY_VALUE_START_WITH);
157     }
158
159     private void checkAndEvaluatePrimitiveProperty(PropertyDefinition propertyDefinition, DataTypeDefinition dataTypeDefinition) {
160         if (ToscaType.isPrimitiveType(dataTypeDefinition.getName()) && CollectionUtils.isNotEmpty(dataTypeDefinition.getConstraints())) {
161             PropertyDefinition definition = new PropertyDefinition();
162             definition.setValue(propertyDefinition.getValue());
163             definition.setType(dataTypeDefinition.getName());
164             definition.setConstraints(dataTypeDefinition.getConstraints());
165             evaluateConstraintsOnProperty(propertyDefinition);
166         }
167     }
168
169     private void evaluateComplexTypeProperties(PropertyDefinition propertyDefinition) {
170         List<PropertyDefinition> propertyDefinitions = dataTypeDefinitionCache.get(propertyDefinition.getType()).getProperties();
171         try {
172             Map<String, Object> valueMap = MapUtils
173                 .emptyIfNull(ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {
174                 }));
175             if (CollectionUtils.isEmpty(propertyDefinitions)) {
176                 checkAndEvaluatePrimitiveProperty(propertyDefinition, dataTypeDefinitionCache.get(propertyDefinition.getType()));
177             } else {
178                 ListUtils.emptyIfNull(propertyDefinitions).forEach(prop -> evaluateRegularComplexType(propertyDefinition, prop, valueMap));
179             }
180         } catch (ConstraintValueDoNotMatchPropertyTypeException e) {
181             logger.debug(e.getMessage(), e);
182             errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, getCompletePropertyName(propertyDefinition)));
183         }
184     }
185
186     private void evaluateRegularComplexType(PropertyDefinition propertyDefinition, PropertyDefinition prop, Map<String, Object> valueMap) {
187         try {
188             PropertyDefinition newPropertyWithValue;
189             if (valueMap.containsKey(prop.getName())) {
190                 if (propertyDefinition.getSubPropertyToscaFunctions() != null) {
191                     for (SubPropertyToscaFunction subPropertyToscaFunction : propertyDefinition.getSubPropertyToscaFunctions()) {
192                         final List<String> path = subPropertyToscaFunction.getSubPropertyPath();
193                         if (path.size() == 1) {
194                             if (path.get(0).equals(prop.getName())) {
195                                 return;
196                             }
197                         }
198                         if (path.size() > 1) {
199                             if (path.get(0).equals(propertyDefinition.getToscaSubPath()) && path.get(1).equals(prop.getName())) {
200                                 return;
201                             }
202                         }
203                     }
204                 }
205                 if (ToscaType.isPrimitiveType(prop.getType())) {
206                     newPropertyWithValue = copyPropertyWithNewValue(prop, String.valueOf(valueMap.get(prop.getName())));
207                     if (isPropertyToEvaluate(newPropertyWithValue)) {
208                         evaluateConstraintsOnProperty(newPropertyWithValue);
209                     }
210                 } else if (ToscaType.isCollectionType(prop.getType())) {
211                     newPropertyWithValue =
212                         copyPropertyWithNewValue(prop,
213                             objectMapper.writeValueAsString(valueMap.get(prop.getName())));
214                     if (isPropertyToEvaluate(newPropertyWithValue)) {
215                         evaluateCollectionTypeProperties(newPropertyWithValue);
216                     }
217                 } else {
218                     newPropertyWithValue =
219                         copyPropertyWithNewValue(prop,
220                             objectMapper.writeValueAsString(valueMap.get(prop.getName())));
221                     if (isPropertyToEvaluate(newPropertyWithValue)) {
222                         evaluateComplexTypeProperties(newPropertyWithValue);
223                     }
224                 }
225             }
226         } catch (IOException | ConstraintValueDoNotMatchPropertyTypeException e) {
227             logger.error(e.getMessage(), e);
228             errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, getCompletePropertyName(propertyDefinition)));
229         }
230     }
231
232     private boolean isPropertyToEvaluate(PropertyDefinition propertyDefinition) throws ConstraintValueDoNotMatchPropertyTypeException {
233         if (Boolean.FALSE.equals(propertyDefinition.isRequired())) {
234             if (!ToscaType.isCollectionType(propertyDefinition.getType())) {
235                 return StringUtils.isNotEmpty(propertyDefinition.getValue()) &&
236                     !"null".equals(propertyDefinition.getValue());
237             } else if (ToscaType.LIST == ToscaType.isValidType(propertyDefinition.getType())) {
238                 Collection<Object> list = ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {
239                 });
240                 return CollectionUtils.isNotEmpty(list);
241             } else {
242                 Map<String, Object> valueMap = MapUtils
243                     .emptyIfNull(ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {
244                     }));
245                 return MapUtils.isNotEmpty(valueMap);
246             }
247         } else {
248             return true;
249         }
250     }
251
252     private void evaluateCollectionTypeProperties(PropertyDefinition propertyDefinition) {
253         ToscaType toscaPropertyType = ToscaType.isValidType(propertyDefinition.getType());
254         try {
255             if (isPropertyToEvaluate(propertyDefinition)) {
256                 evaluateCollectionConstraints(propertyDefinition, toscaPropertyType);
257             }
258         } catch (ConstraintValueDoNotMatchPropertyTypeException e) {
259             logger.error(e.getMessage(), e);
260             errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, getCompletePropertyName(propertyDefinition)));
261         }
262         if (ToscaType.LIST == toscaPropertyType) {
263             evaluateListType(propertyDefinition);
264         } else if (ToscaType.MAP == toscaPropertyType) {
265             evaluateMapType(propertyDefinition);
266         }
267     }
268
269     private void evaluateCollectionConstraints(PropertyDefinition propertyDefinition, ToscaType toscaPropertyType) {
270         List<PropertyConstraint> constraintsList = propertyDefinition.getConstraints();
271
272         if (CollectionUtils.isEmpty(constraintsList)) {
273             return;
274         }
275         ToscaType toscaPropertyType1;
276         if (null == toscaPropertyType) {
277             toscaPropertyType1 = ToscaType.isValidType(propertyDefinition.getType());
278         } else {
279             toscaPropertyType1 = toscaPropertyType;
280         }
281         constraintsList.stream()
282             .filter(this::isACollectionConstraint)
283             .forEach(propertyConstraint -> {
284                 try {
285                     if (ToscaType.LIST == toscaPropertyType1) {
286                         Collection<Object> list = ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {
287                         });
288                         propertyConstraint.validate(list);
289                     } else if (ToscaType.MAP == toscaPropertyType1) {
290                         final Map<String, Object> map = ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {
291                         });
292                         propertyConstraint.validate(map);
293                     }
294                 } catch (ConstraintValueDoNotMatchPropertyTypeException | ConstraintViolationException exception) {
295                     errorMessages.add("\n" + propertyConstraint.getErrorMessage(toscaPropertyType1, exception,
296                         getCompletePropertyName(propertyDefinition)));
297                 }
298             });
299     }
300
301     private boolean isACollectionConstraint(PropertyConstraint constraint) {
302         if (constraint instanceof MaxLengthConstraint) {
303             return true;
304         }
305         if (constraint instanceof MinLengthConstraint) {
306             return true;
307         }
308         return constraint instanceof LengthConstraint;
309     }
310
311     private void evaluateListType(PropertyDefinition propertyDefinition) {
312         try {
313             if (propertyDefinition.getSchemaType() == null) {
314                 propertyDefinition.setSchema(createStringSchema());
315             }
316             Collection<Object> list = ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {});
317             final Map<String, Object> map = new HashMap<>();
318             int index = 0;
319             for (Object obj : list) {
320                 map.put(String.valueOf(index),obj);
321                 index++;
322             }
323             evaluateCollectionType(propertyDefinition, map);
324         } catch (ConstraintValueDoNotMatchPropertyTypeException e) {
325             logger.debug(e.getMessage(), e);
326             errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, getCompletePropertyName(propertyDefinition)));
327         }
328     }
329
330     private SchemaDefinition createStringSchema() {
331         final SchemaDefinition schemaDefinition = new SchemaDefinition();
332         final PropertyDefinition schemaStringProperty = new PropertyDefinition();
333         schemaStringProperty.setType(ToscaType.STRING.getType());
334         schemaDefinition.setProperty(schemaStringProperty);
335         return schemaDefinition;
336     }
337
338     private void evaluateMapType(final PropertyDefinition propertyDefinition) {
339         try {
340             if (propertyDefinition.getSchemaType() == null) {
341                 propertyDefinition.setSchema(createStringSchema());
342             }
343             final Map<String, Object> map = ConstraintUtil.parseToCollection(propertyDefinition.getValue(), new TypeReference<>() {
344             });
345             evaluateCollectionType(propertyDefinition, map);
346         } catch (ConstraintValueDoNotMatchPropertyTypeException e) {
347             logger.debug(e.getMessage(), e);
348             errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, getCompletePropertyName(propertyDefinition)));
349         }
350     }
351
352     private void evaluateCollectionPrimitiveSchemaType(final PropertyDefinition propertyDefinition,
353                                                        final String schemaType) throws JsonProcessingException {
354         if (propertyDefinition.getSchema() != null && propertyDefinition.getSchema().getProperty() instanceof PropertyDefinition) {
355             propertyDefinition.setConstraints(((PropertyDefinition) propertyDefinition.getSchema().getProperty()).getConstraints());
356             propertyDefinition.setValue(objectMapper.readValue(propertyDefinition.getValue(), String.class));
357             propertyDefinition.setType(schemaType);
358             evaluateConstraintsOnProperty(propertyDefinition);
359         }
360     }
361
362     private void evaluateCollectionType(final PropertyDefinition propertyDefinition, final Map<String, Object> valueMap) {
363         final String schemaType = propertyDefinition.getSchemaType();
364         for (String mapKey : valueMap.keySet()) {
365             final Object value = valueMap.get(mapKey);
366             try {
367                 final PropertyDefinition propertyCopyWithNewValue = copyPropertyWithNewValue(propertyDefinition,
368                     objectMapper.writeValueAsString(value));
369                 propertyCopyWithNewValue.setToscaSubPath(mapKey);
370                 if (ToscaType.isPrimitiveType(schemaType)) {
371                     evaluateCollectionPrimitiveSchemaType(propertyCopyWithNewValue, schemaType);
372                 } else if (ToscaType.isCollectionType(schemaType)) {
373                     propertyCopyWithNewValue.setType(schemaType);
374                     propertyCopyWithNewValue.setSchemaType(propertyDefinition.getSchemaProperty().getSchemaType());
375                     evaluateCollectionTypeProperties(propertyCopyWithNewValue);
376                 } else {
377                     propertyCopyWithNewValue.setType(schemaType);
378                     completePropertyName.append(UNDERSCORE);
379                     completePropertyName.append(propertyCopyWithNewValue.getName());
380                     evaluateComplexTypeProperties(propertyCopyWithNewValue);
381                 }
382             } catch (final Exception e) {
383                 logger.debug(e.getMessage(), e);
384                 errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, getCompletePropertyName(propertyDefinition)));
385             }
386         }
387     }
388
389     private String getCompletePropertyName(final PropertyDefinition propertyDefinition) {
390         if (StringUtils.isNotBlank(completeInputName)) {
391             return completeInputName;
392         }
393
394         final String propertyName = propertyDefinition == null ? "" : propertyDefinition.getName();
395         if (StringUtils.isNotBlank(completePropertyName)) {
396             return completePropertyName + UNDERSCORE + propertyName;
397         }
398
399         return propertyName;
400     }
401
402     private PropertyDefinition copyPropertyWithNewValue(final PropertyDefinition propertyToCopy, final String value) {
403         final var propertyDefinition = new PropertyDefinition(propertyToCopy);
404         propertyDefinition.setValue(value);
405         return propertyDefinition;
406     }
407
408     private PropertyDefinition getPropertyDefinitionObjectFromInputs(PropertyDefinition property) {
409         InputDefinition inputDefinition = (InputDefinition) property;
410         PropertyDefinition propertyDefinition = null;
411         if (CollectionUtils.isEmpty(inputDefinition.getProperties()) || ToscaType.isPrimitiveType(inputDefinition.getProperties().get(0).getType())) {
412             propertyDefinition = new PropertyDefinition();
413             propertyDefinition.setType(inputDefinition.getType());
414             propertyDefinition.setValue(inputDefinition.getDefaultValue());
415             propertyDefinition.setName(inputDefinition.getName());
416             propertyDefinition.setConstraints(inputDefinition.getConstraints());
417         } else if (Objects.nonNull(inputDefinition.getInputPath())) {
418             propertyDefinition = evaluateComplexTypeInputs(inputDefinition);
419             propertyDefinition.setConstraints(inputDefinition.getConstraints());
420         }
421         return propertyDefinition;
422     }
423
424     private PropertyDefinition evaluateComplexTypeInputs(InputDefinition inputDefinition) {
425         Map<String, Object> inputMap = new HashMap<>();
426         PropertyDefinition propertyDefinition = new PropertyDefinition();
427         String[] inputPathArr = inputDefinition.getInputPath().split("#");
428         if (inputPathArr.length > 1) {
429             inputPathArr = ArrayUtils.remove(inputPathArr, 0);
430         }
431         try {
432             Map<String, Object> presentMap = inputMap;
433             for (int i = 0; i < inputPathArr.length; i++) {
434                 if (i == inputPathArr.length - 1) {
435                     presentMap.computeIfAbsent(inputPathArr[i], k -> inputDefinition.getDefaultValue());
436                 } else {
437                     presentMap.computeIfAbsent(inputPathArr[i], k -> new HashMap<String, Object>());
438                     presentMap = (Map<String, Object>) presentMap.get(inputPathArr[i]);
439                 }
440             }
441             if (CollectionUtils.isNotEmpty(inputDefinition.getProperties())) {
442                 propertyDefinition.setType(inputDefinition.getProperties().get(0).getType());
443             }
444             propertyDefinition.setName(inputDefinition.getName());
445             propertyDefinition.setValue(objectMapper.writeValueAsString(inputMap));
446         } catch (IOException e) {
447             logger.error(e.getMessage(), e);
448             errorMessages.add(String.format(VALUE_PROVIDED_IN_INVALID_FORMAT_FOR_PROPERTY, inputDefinition.getName()));
449         }
450         return propertyDefinition;
451     }
452
453     protected ResponseFormatManager getResponseFormatManager() {
454         return ResponseFormatManager.getInstance();
455     }
456 }