Fix 'Unable to delete declared outputs'
[sdc.git] / catalog-be / src / main / java / org / openecomp / sdc / be / components / attribute / DefaultAttributeDeclarator.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * SDC
4  * ================================================================================
5  * Copyright (C) 2021, Nordix Foundation. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20 package org.openecomp.sdc.be.components.attribute;
21
22 import static org.openecomp.sdc.common.api.Constants.GET_ATTRIBUTE;
23
24 import com.google.gson.Gson;
25 import fj.data.Either;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Collection;
29 import java.util.HashMap;
30 import java.util.HashSet;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Objects;
34 import java.util.Optional;
35 import java.util.Set;
36 import java.util.stream.Collectors;
37 import org.apache.commons.collections4.CollectionUtils;
38 import org.apache.commons.collections4.MapUtils;
39 import org.apache.commons.lang3.StringUtils;
40 import org.json.simple.JSONObject;
41 import org.openecomp.sdc.be.datatypes.elements.AttributeDataDefinition;
42 import org.openecomp.sdc.be.datatypes.elements.GetOutputValueDataDefinition;
43 import org.openecomp.sdc.be.datatypes.elements.PropertiesOwner;
44 import org.openecomp.sdc.be.model.AttributeDefinition;
45 import org.openecomp.sdc.be.model.Component;
46 import org.openecomp.sdc.be.model.ComponentInstanceAttribOutput;
47 import org.openecomp.sdc.be.model.IComponentInstanceConnectedElement;
48 import org.openecomp.sdc.be.model.OutputDefinition;
49 import org.openecomp.sdc.be.model.operations.api.StorageOperationStatus;
50 import org.openecomp.sdc.be.model.operations.impl.UniqueIdBuilder;
51 import org.openecomp.sdc.common.log.wrappers.Logger;
52 import org.yaml.snakeyaml.Yaml;
53
54 public abstract class DefaultAttributeDeclarator<PROPERTYOWNER extends PropertiesOwner, ATTRIBUTETYPE extends AttributeDataDefinition> implements
55     AttributeDeclarator {
56
57     private static final Logger log = Logger.getLogger(DefaultAttributeDeclarator.class);
58     private static final String UNDERSCORE = "_";
59     private final Gson gson = new Gson();
60
61     protected DefaultAttributeDeclarator() {
62     }
63
64     @Override
65     public Either<List<OutputDefinition>, StorageOperationStatus> declareAttributesAsOutputs(final Component component,
66                                                                                              final String propertiesOwnerId,
67                                                                                              final List<ComponentInstanceAttribOutput> attribsToDeclare) {
68         log.debug("#declarePropertiesAsInputs - declaring properties as inputs for component {} from properties owner {}", component.getUniqueId(),
69             propertiesOwnerId);
70         return resolvePropertiesOwner(component, propertiesOwnerId)
71             .map(propertyOwner -> declareAttributesAsOutputs(component, propertyOwner, attribsToDeclare))
72             .orElse(Either.right(onPropertiesOwnerNotFound(component.getUniqueId(), propertiesOwnerId)));
73     }
74
75     protected abstract ATTRIBUTETYPE createDeclaredAttribute(final AttributeDataDefinition attributeDataDefinition);
76
77     protected abstract Either<?, StorageOperationStatus> updateAttributesValues(final Component component, final String propertiesOwnerId,
78                                                                                 final List<ATTRIBUTETYPE> attributetypeList);
79
80     protected abstract Optional<PROPERTYOWNER> resolvePropertiesOwner(final Component component, final String propertiesOwnerId);
81
82     private StorageOperationStatus onPropertiesOwnerNotFound(final String componentId, final String propertiesOwnerId) {
83         log.debug("#declarePropertiesAsInputs - properties owner {} was not found on component {}", propertiesOwnerId, componentId);
84         return StorageOperationStatus.NOT_FOUND;
85     }
86
87     private Either<List<OutputDefinition>, StorageOperationStatus> declareAttributesAsOutputs(final Component component,
88                                                                                               final PROPERTYOWNER propertiesOwner,
89                                                                                               final List<ComponentInstanceAttribOutput> attributesToDeclare) {
90         final AttributesDeclarationData attributesDeclarationData = createOutputsAndOverrideAttributesValues(component, propertiesOwner,
91             attributesToDeclare);
92         return updateAttributesValues(component, propertiesOwner.getUniqueId(), attributesDeclarationData.getAttributesToUpdate()).left()
93             .map(updatePropsRes -> attributesDeclarationData.getOutputsToCreate());
94     }
95
96     private AttributesDeclarationData createOutputsAndOverrideAttributesValues(final Component component, final PROPERTYOWNER propertiesOwner,
97                                                                                final List<ComponentInstanceAttribOutput> attributesToDeclare) {
98         final List<ATTRIBUTETYPE> declaredAttributes = new ArrayList<>();
99         final List<OutputDefinition> createdInputs = attributesToDeclare.stream()
100             .map(attributeOutput -> declareAttributeOutput(component, propertiesOwner, declaredAttributes, attributeOutput))
101             .collect(Collectors.toList());
102         return new AttributesDeclarationData(createdInputs, declaredAttributes);
103     }
104
105     private OutputDefinition declareAttributeOutput(final Component component, final PROPERTYOWNER propertiesOwner,
106                                                     final List<ATTRIBUTETYPE> declaredAttributes, final ComponentInstanceAttribOutput attribOutput) {
107         final AttributeDataDefinition attribute = resolveAttribute(declaredAttributes, attribOutput);
108         final OutputDefinition outputDefinition = createOutput(component, propertiesOwner, attribOutput, attribute);
109         final ATTRIBUTETYPE declaredAttribute = createDeclaredAttribute(attribute);
110         if (!declaredAttributes.contains(declaredAttribute)) {
111             declaredAttributes.add(declaredAttribute);
112         }
113         return outputDefinition;
114     }
115
116     private OutputDefinition createOutput(final Component component, final PROPERTYOWNER propertiesOwner,
117                                           final ComponentInstanceAttribOutput attribOutput, final AttributeDataDefinition attributeDataDefinition) {
118         String generatedInputPrefix = propertiesOwner.getNormalizedName();
119         if (propertiesOwner.getUniqueId().equals(attribOutput.getParentUniqueId())) {
120             //Creating input from property create on self using add property..Do not add the prefix
121             generatedInputPrefix = null;
122         }
123         final String generatedOutputName = generateOutputName(generatedInputPrefix, attribOutput);
124         log.debug("createInput: propOwner.uniqueId={}, attribOutput.parentUniqueId={}", propertiesOwner.getUniqueId(),
125             attribOutput.getParentUniqueId());
126         return createOutputFromAttribute(component.getUniqueId(), propertiesOwner, generatedOutputName, attribOutput, attributeDataDefinition);
127     }
128
129     private String generateOutputName(final String outputName, final ComponentInstanceAttribOutput attribOutput) {
130         final String declaredInputName;
131         final String[] parsedPropNames = attribOutput.getParsedAttribNames();
132         if (parsedPropNames != null) {
133             declaredInputName = handleInputName(outputName, parsedPropNames);
134         } else {
135             final String[] propName = {attribOutput.getName()};
136             declaredInputName = handleInputName(outputName, propName);
137         }
138         return declaredInputName;
139     }
140
141     private String handleInputName(final String outputName, final String[] parsedPropNames) {
142         final StringBuilder prefix = new StringBuilder();
143         int startingIndex;
144         if (Objects.isNull(outputName)) {
145             prefix.append(parsedPropNames[0]);
146             startingIndex = 1;
147         } else {
148             prefix.append(outputName);
149             startingIndex = 0;
150         }
151         while (startingIndex < parsedPropNames.length) {
152             prefix.append(UNDERSCORE);
153             prefix.append(parsedPropNames[startingIndex]);
154             startingIndex++;
155         }
156         return prefix.toString();
157     }
158
159     private AttributeDataDefinition resolveAttribute(final List<ATTRIBUTETYPE> attributesToCreate, final ComponentInstanceAttribOutput attribOutput) {
160         final Optional<ATTRIBUTETYPE> resolvedAttribute = attributesToCreate.stream().filter(p -> p.getName().equals(attribOutput.getName()))
161             .findFirst();
162         return resolvedAttribute.isPresent() ? resolvedAttribute.get() : attribOutput;
163     }
164
165     OutputDefinition createOutputFromAttribute(final String componentId, final PROPERTYOWNER propertiesOwner, final String outputName,
166                                                final ComponentInstanceAttribOutput attributeOutput, final AttributeDataDefinition attribute) {
167         final String attributesName = attributeOutput.getAttributesName();
168         final AttributeDefinition selectedAttrib = attributeOutput.getOutput();
169         final String[] parsedAttribNames = attributeOutput.getParsedAttribNames();
170         OutputDefinition outputDefinition;
171         boolean complexProperty = false;
172         if (attributesName != null && !attributesName.isEmpty() && selectedAttrib != null) {
173             complexProperty = true;
174             outputDefinition = new OutputDefinition(selectedAttrib);
175             outputDefinition.setDefaultValue(selectedAttrib.getValue());
176         } else {
177             outputDefinition = new OutputDefinition(attribute);
178             outputDefinition.setDefaultValue(attribute.getValue());
179         }
180         outputDefinition.setName(outputName);
181         outputDefinition.setUniqueId(UniqueIdBuilder.buildPropertyUniqueId(componentId, outputDefinition.getName()));
182         outputDefinition.setOutputPath(attributesName);
183         outputDefinition.setInstanceUniqueId(propertiesOwner.getUniqueId());
184         outputDefinition.setAttributeId(attributeOutput.getUniqueId());
185         outputDefinition.setAttributes(Arrays.asList(attributeOutput));
186         if (attribute instanceof IComponentInstanceConnectedElement) {
187             ((IComponentInstanceConnectedElement) attribute).setComponentInstanceId(propertiesOwner.getUniqueId());
188             ((IComponentInstanceConnectedElement) attribute).setComponentInstanceName(propertiesOwner.getName());
189         }
190         changeOutputValueToGetAttributeValue(outputName, parsedAttribNames, outputDefinition, attribute, complexProperty);
191         return outputDefinition;
192     }
193
194     private void changeOutputValueToGetAttributeValue(final String outputName, final String[] parsedPropNames, final OutputDefinition output,
195                                                       final AttributeDataDefinition attributeDataDefinition, final boolean complexProperty) {
196         JSONObject jsonObject = new JSONObject();
197         final String value = attributeDataDefinition.getValue();
198         if (StringUtils.isEmpty(value)) {
199             if (complexProperty) {
200                 jsonObject = createJSONValueForProperty(parsedPropNames.length - 1, parsedPropNames, jsonObject, outputName);
201                 attributeDataDefinition.setValue(jsonObject.toJSONString());
202             } else {
203                 jsonObject
204                     .put(GET_ATTRIBUTE, Arrays.asList(output.getAttributes().get(0).getComponentInstanceName(), attributeDataDefinition.getName()));
205                 output.setValue(jsonObject.toJSONString());
206             }
207         } else {
208             final Object objValue = new Yaml().load(value);
209             if (objValue instanceof Map || objValue instanceof List) {
210                 if (!complexProperty) {
211                     jsonObject.put(GET_ATTRIBUTE,
212                         Arrays.asList(output.getAttributes().get(0).getComponentInstanceName(), attributeDataDefinition.getName()));
213                     output.setValue(jsonObject.toJSONString());
214                 } else {
215                     final Map<String, Object> mappedToscaTemplate = (Map<String, Object>) objValue;
216                     createOutputValue(mappedToscaTemplate, 1, parsedPropNames, outputName);
217                     output.setValue(gson.toJson(mappedToscaTemplate));
218                 }
219             } else {
220                 jsonObject
221                     .put(GET_ATTRIBUTE, Arrays.asList(output.getAttributes().get(0).getComponentInstanceName(), attributeDataDefinition.getName()));
222                 output.setValue(jsonObject.toJSONString());
223             }
224         }
225         if (CollectionUtils.isEmpty(attributeDataDefinition.getGetOutputValues())) {
226             attributeDataDefinition.setGetOutputValues(new ArrayList<>());
227         }
228         final List<GetOutputValueDataDefinition> getOutputValues = attributeDataDefinition.getGetOutputValues();
229         final GetOutputValueDataDefinition getOutputValueDataDefinition = new GetOutputValueDataDefinition();
230         getOutputValueDataDefinition.setOutputId(output.getUniqueId());
231         getOutputValueDataDefinition.setOutputName(output.getName());
232         getOutputValues.add(getOutputValueDataDefinition);
233     }
234
235     private JSONObject createJSONValueForProperty(int i, final String[] parsedPropNames, final JSONObject ooj, final String outputName) {
236         while (i >= 1) {
237             if (i == parsedPropNames.length - 1) {
238                 final JSONObject jobProp = new JSONObject();
239                 jobProp.put(GET_ATTRIBUTE, outputName);
240                 ooj.put(parsedPropNames[i], jobProp);
241                 i--;
242                 return createJSONValueForProperty(i, parsedPropNames, ooj, outputName);
243             } else {
244                 final JSONObject res = new JSONObject();
245                 res.put(parsedPropNames[i], ooj);
246                 i--;
247                 return createJSONValueForProperty(i, parsedPropNames, res, outputName);
248             }
249         }
250         return ooj;
251     }
252
253     private Map<String, Object> createOutputValue(final Map<String, Object> lhm1, int index, final String[] outputNames, final String outputName) {
254         while (index < outputNames.length) {
255             if (lhm1.containsKey(outputNames[index])) {
256                 final Object value = lhm1.get(outputNames[index]);
257                 if (value instanceof Map) {
258                     if (index == outputNames.length - 1) {
259                         return (Map<String, Object>) ((Map) value).put(GET_ATTRIBUTE, outputName);
260                     } else {
261                         return createOutputValue((Map) value, ++index, outputNames, outputName);
262                     }
263                 } else {
264                     final Map<String, Object> jobProp = new HashMap<>();
265                     if (index == outputNames.length - 1) {
266                         jobProp.put(GET_ATTRIBUTE, outputName);
267                         lhm1.put(outputNames[index], jobProp);
268                         return lhm1;
269                     } else {
270                         lhm1.put(outputNames[index], jobProp);
271                         return createOutputValue(jobProp, ++index, outputNames, outputName);
272                     }
273                 }
274             } else {
275                 final Map<String, Object> jobProp = new HashMap<>();
276                 lhm1.put(outputNames[index], jobProp);
277                 if (index == outputNames.length - 1) {
278                     jobProp.put(GET_ATTRIBUTE, outputName);
279                     return jobProp;
280                 } else {
281                     return createOutputValue(jobProp, ++index, outputNames, outputName);
282                 }
283             }
284         }
285         return lhm1;
286     }
287
288     private void resetOutputName(final Map<String, Object> lhm1, final String outputName) {
289         for (final Map.Entry<String, Object> entry : lhm1.entrySet()) {
290             final String key = entry.getKey();
291             final Object value = entry.getValue();
292             if (value instanceof String && ((String) value).equalsIgnoreCase(outputName) && GET_ATTRIBUTE.equals(key)) {
293                 lhm1.remove(key);
294             } else if (value instanceof Map) {
295                 final Map<String, Object> subMap = (Map<String, Object>) value;
296                 resetOutputName(subMap, outputName);
297             } else if (value instanceof List && ((List) value).contains(outputName) && GET_ATTRIBUTE.equals(key)) {
298                 lhm1.remove(key);
299             }
300         }
301     }
302
303     /*        Mutates the object
304      *        Tail recurse -> traverse the tosca elements and remove nested empty map properties
305      *        this only handles nested maps, other objects are left untouched (even a Set containing a map) since behaviour is unexpected
306      *
307      *        @param  toscaElement - expected map of tosca values
308      *        @return mutated @param toscaElement , where empty maps are deleted , return null for empty map.
309      **/
310     private Object cleanEmptyNestedValuesInMap(final Object toscaElement, short loopProtectionLevel) {
311         if (loopProtectionLevel <= 0 || toscaElement == null || !(toscaElement instanceof Map)) {
312             return toscaElement;
313         }
314         if (MapUtils.isNotEmpty((Map) toscaElement)) {
315             Object ret;
316             final Set<Object> keysToRemove = new HashSet<>();                                                                 // use different set to avoid ConcurrentModificationException
317             for (final Object key : ((Map) toscaElement).keySet()) {
318                 final Object value = ((Map) toscaElement).get(key);
319                 ret = cleanEmptyNestedValuesInMap(value, --loopProtectionLevel);
320                 if (ret == null) {
321                     keysToRemove.add(key);
322                 }
323             }
324             final Collection set = ((Map) toscaElement).keySet();
325             if (CollectionUtils.isNotEmpty(set)) {
326                 set.removeAll(keysToRemove);
327             }
328             if (isEmptyNestedMap(toscaElement)) {
329                 return null;
330             }
331         } else {
332             return null;
333         }
334         return toscaElement;
335     }
336
337     /**
338      * @param element
339      * @return true if map nested maps are all empty, ignores other collection objects
340      */
341     private boolean isEmptyNestedMap(final Object element) {
342         boolean isEmpty = true;
343         if (element != null) {
344             if (element instanceof Map) {
345                 if (MapUtils.isNotEmpty((Map) element)) {
346                     for (final Object key : ((Map) (element)).keySet()) {
347                         Object value = ((Map) (element)).get(key);
348                         isEmpty &= isEmptyNestedMap(value);
349                     }
350                 }
351             } else {
352                 isEmpty = false;
353             }
354         }
355         return isEmpty;
356     }
357
358     private class AttributesDeclarationData {
359
360         private final List<OutputDefinition> outputsToCreate;
361         private final List<ATTRIBUTETYPE> attributesToUpdate;
362
363         AttributesDeclarationData(final List<OutputDefinition> outputsToCreate, final List<ATTRIBUTETYPE> attributesToUpdate) {
364             this.outputsToCreate = outputsToCreate;
365             this.attributesToUpdate = attributesToUpdate;
366         }
367
368         List<OutputDefinition> getOutputsToCreate() {
369             return outputsToCreate;
370         }
371
372         List<ATTRIBUTETYPE> getAttributesToUpdate() {
373             return attributesToUpdate;
374         }
375     }
376 }