Aligned attributes of CDS components
[ccsdk/cds.git] / ms / blueprintsprocessor / functions / config-snapshots / src / main / kotlin / org / onap / ccsdk / cds / blueprintsprocessor / functions / config / snapshots / ComponentConfigSnapshotsExecutor.kt
1 /*
2  *  Copyright © 2019 Bell Canada.
3  *  Modifications Copyright © 2018-2019 IBM.
4  *
5  *  Licensed under the Apache License, Version 2.0 (the "License");
6  *  you may not use this file except in compliance with the License.
7  *  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  */
17
18 package org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots
19
20 import com.github.fge.jsonpatch.diff.JsonDiff
21 import org.apache.logging.log4j.util.Strings
22 import org.onap.ccsdk.cds.blueprintsprocessor.core.api.data.ExecutionServiceInput
23 import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot
24 import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot.Status.CANDIDATE
25 import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot.Status.RUNNING
26 import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshotService
27 import org.onap.ccsdk.cds.blueprintsprocessor.services.execution.AbstractComponentFunction
28 import org.onap.ccsdk.cds.controllerblueprints.core.asJsonPrimitive
29 import org.onap.ccsdk.cds.controllerblueprints.core.jsonAsJsonType
30 import org.onap.ccsdk.cds.controllerblueprints.core.returnNullIfMissing
31 import org.slf4j.LoggerFactory
32 import org.springframework.beans.factory.config.ConfigurableBeanFactory
33 import org.springframework.context.annotation.Scope
34 import org.springframework.stereotype.Component
35 import org.w3c.dom.Node
36 import org.xmlunit.builder.DiffBuilder
37 import org.xmlunit.builder.Input
38 import org.xmlunit.diff.ComparisonType
39 import org.xmlunit.diff.Diff
40
41 /**
42  * ComponentConfigSnapshotsExecutor
43  *
44  * Component that retrieves the saved configuration snapshot as identified by the input parameters,
45  * named resource-id and resource-type.
46  *
47  * It reports the content of the requested snapshot via properties, config-snapshot-status
48  * and config-snapshot-value.  In case of error, details can be found in the config-snapshot-status
49  * and config-snapshot-message properties
50  *
51  * @author Serge Simard
52  */
53 @Component("component-config-snapshots-executor")
54 @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
55 open class ComponentConfigSnapshotsExecutor(private val cfgSnapshotService: ResourceConfigSnapshotService) :
56     AbstractComponentFunction() {
57
58     companion object {
59
60         private val log = LoggerFactory.getLogger(ComponentConfigSnapshotsExecutor::class.java)
61
62         // input fields names accepted by this executor
63         const val INPUT_OPERATION = "operation"
64         const val INPUT_RESOURCE_ID = "resource-id"
65         const val INPUT_RESOURCE_TYPE = "resource-type"
66         const val INPUT_RESOURCE_STATUS = "resource-status"
67         const val INPUT_RESOURCE_SNAPSHOT = "resource-snapshot"
68         const val INPUT_DIFF_CONTENT_TYPE = "diff-content-type"
69
70         const val OPERATION_FETCH = "fetch"
71         const val OPERATION_STORE = "store"
72         const val OPERATION_DIFF = "diff"
73
74         const val DIFF_JSON = "JSON"
75         const val DIFF_XML = "XML"
76
77         // output fields names (and values) populated by this executor.
78         const val ATTRIBUTE_STATUS = "config-snapshot-status"
79         const val ATTRIBUTE_MESSAGE = "config-snapshot-message"
80         const val ATTRIBUTE_SNAPSHOT = "config-snapshot-value"
81
82         const val ATTRIBUTE_STATUS_SUCCESS = "success"
83         const val ATTRIBUTE_STATUS_ERROR = "error"
84     }
85
86     /**
87      * Main entry point for ComponentConfigSnapshotsExecutor
88      * Supports 3 operations : fetch, store or diff
89      */
90     override suspend fun processNB(executionRequest: ExecutionServiceInput) {
91
92         val operation = getOptionalOperationInput(INPUT_OPERATION)?.returnNullIfMissing()?.textValue() ?: ""
93         val contentType = getOptionalOperationInput(INPUT_DIFF_CONTENT_TYPE)?.returnNullIfMissing()?.textValue() ?: ""
94         val resourceId = getOptionalOperationInput(INPUT_RESOURCE_ID)?.returnNullIfMissing()?.textValue() ?: ""
95         val resourceType = getOptionalOperationInput(INPUT_RESOURCE_TYPE)?.returnNullIfMissing()?.textValue() ?: ""
96         val resourceStatus = getOptionalOperationInput(INPUT_RESOURCE_STATUS)?.returnNullIfMissing()?.textValue() ?: RUNNING.name
97         val snapshot = getOptionalOperationInput(INPUT_RESOURCE_SNAPSHOT)?.returnNullIfMissing()?.textValue() ?: ""
98         val status = ResourceConfigSnapshot.Status.valueOf(resourceStatus)
99
100         when (operation) {
101             OPERATION_FETCH -> fetchConfigurationSnapshot(resourceId, resourceType, status)
102             OPERATION_STORE -> storeConfigurationSnapshot(snapshot, resourceId, resourceType, status)
103             OPERATION_DIFF -> compareConfigurationSnapshot(resourceId, resourceType, contentType)
104
105             else -> setNodeOutputErrors(
106                 ATTRIBUTE_STATUS_ERROR,
107                 "Operation parameter must be fetch, store or diff"
108             )
109         }
110     }
111
112     /**
113      * General error handling for the executor.
114      */
115     override suspend fun recoverNB(runtimeException: RuntimeException, executionRequest: ExecutionServiceInput) {
116         setNodeOutputErrors(ATTRIBUTE_STATUS_ERROR, "Error : ${runtimeException.message}")
117     }
118
119     /**
120      * Fetch a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
121      */
122     private suspend fun fetchConfigurationSnapshot(
123         resourceId: String,
124         resourceType: String,
125         status: ResourceConfigSnapshot.Status = RUNNING
126     ) {
127         try {
128             val cfgSnapshotValue = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, status)
129             setNodeOutputProperties(ATTRIBUTE_STATUS_SUCCESS, cfgSnapshotValue)
130         } catch (er: NoSuchElementException) {
131             val message = "No Resource config snapshot identified by resourceId={$resourceId}, " +
132                 "resourceType={$resourceType} does not exists"
133             setNodeOutputErrors(ATTRIBUTE_STATUS_ERROR, message)
134         }
135     }
136
137     /**
138      * Store a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
139      */
140     private suspend fun storeConfigurationSnapshot(
141         cfgSnapshotValue: String,
142         resourceId: String,
143         resourceType: String,
144         status: ResourceConfigSnapshot.Status = RUNNING
145     ) {
146         if (cfgSnapshotValue.isNotEmpty()) {
147             val cfgSnapshotSaved = cfgSnapshotService.write(cfgSnapshotValue, resourceId, resourceType, status)
148             setNodeOutputProperties(ATTRIBUTE_STATUS_SUCCESS, cfgSnapshotSaved.config_snapshot ?: "")
149         } else {
150             val message = "Could not store config snapshot identified by resourceId={$resourceId},resourceType={$resourceType} does not exists"
151             setNodeOutputErrors(ATTRIBUTE_STATUS_ERROR, message)
152         }
153     }
154
155     /**
156      * Compare two configs (RUNNING vs CANDIDATE) for resource identified by ID/type, using the specified contentType
157      */
158     private suspend fun compareConfigurationSnapshot(resourceId: String, resourceType: String, contentType: String) {
159
160         val cfgRunning = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, RUNNING)
161         val cfgCandidate = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, CANDIDATE)
162
163         if (cfgRunning.isEmpty() || cfgCandidate.isEmpty()) {
164             setNodeOutputProperties(ATTRIBUTE_STATUS_SUCCESS, Strings.EMPTY)
165             return
166         }
167
168         when (contentType.toUpperCase()) {
169             DIFF_JSON -> {
170                 val patchNode = JsonDiff.asJson(cfgRunning.jsonAsJsonType(), cfgCandidate.jsonAsJsonType())
171                 setNodeOutputProperties(ATTRIBUTE_STATUS_SUCCESS, patchNode.toString())
172             }
173             DIFF_XML -> {
174                 val myDiff = DiffBuilder
175                     .compare(Input.fromString(cfgRunning))
176                     .withTest(Input.fromString(cfgCandidate))
177                     .checkForSimilar()
178                     .ignoreComments()
179                     .ignoreWhitespace()
180                     .normalizeWhitespace()
181                     .build()
182
183                 setNodeOutputProperties(ATTRIBUTE_STATUS_SUCCESS, formatXmlDifferences(myDiff))
184             }
185             else -> {
186                 val message = "Could not compare config snapshots for type $contentType"
187                 setNodeOutputErrors(ATTRIBUTE_STATUS_ERROR, message)
188             }
189         }
190     }
191
192     /**
193      * Utility function to set the output properties of the executor node
194      */
195     private fun setNodeOutputProperties(status: String, snapshot: String) {
196         setAttribute(ATTRIBUTE_STATUS, status.asJsonPrimitive())
197         setAttribute(ATTRIBUTE_SNAPSHOT, snapshot.asJsonPrimitive())
198         log.debug("Setting output $ATTRIBUTE_STATUS=$status")
199     }
200
201     /**
202      * Utility function to set the output properties and errors of the executor node, in case of errors
203      */
204     private fun setNodeOutputErrors(status: String, message: String) {
205         setAttribute(ATTRIBUTE_STATUS, status.asJsonPrimitive())
206         setAttribute(ATTRIBUTE_MESSAGE, message.asJsonPrimitive())
207         setAttribute(ATTRIBUTE_SNAPSHOT, "".asJsonPrimitive())
208
209         log.info("Setting error and output $ATTRIBUTE_STATUS=$status, $ATTRIBUTE_MESSAGE=$message ")
210
211         addError(status, ATTRIBUTE_MESSAGE, message)
212     }
213
214     /**
215      * Formats XmlUnit differences into xml-patch like response (RFC5261)
216      */
217     private fun formatXmlDifferences(differences: Diff): String {
218         val output = StringBuilder()
219         output.append(
220             "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
221                 "<diff>"
222         )
223         val diffIterator = differences.getDifferences().iterator()
224         while (diffIterator.hasNext()) {
225
226             val aDiff = diffIterator.next().comparison
227             when (aDiff.type) {
228                 ComparisonType.ATTR_VALUE -> {
229                     output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
230                         .append(aDiff.testDetails.value)
231                         .append("</replace>")
232                 }
233                 ComparisonType.TEXT_VALUE -> {
234                     output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
235                         .append(aDiff.testDetails.value)
236                         .append("</replace>")
237                 }
238                 ComparisonType.CHILD_LOOKUP -> {
239                     output.append("<add sel=\"").append(aDiff.testDetails.parentXPath).append("\">")
240                         .append(formatNode(aDiff.testDetails.target))
241                         .append("</add>")
242                 }
243                 ComparisonType.CHILD_NODELIST_LENGTH -> {
244                     // Ignored; will be processed in the CHILD_LOOKUP case
245                 }
246                 else -> {
247                     log.warn("Unsupported XML difference found: $aDiff")
248                 }
249             }
250         }
251         output.append("</diff>")
252
253         return output.toString()
254     }
255
256     /**
257      * Formats a node value obtained from an XmlUnit differences node
258      */
259     private fun formatNode(node: Node): String {
260         val output = StringBuilder()
261
262         val parentName = node.localName
263         output.append("<$parentName>")
264         if (node.hasChildNodes()) {
265             val nodes = node.childNodes
266             for (index in 1..nodes.length) {
267                 val child = nodes.item(index - 1)
268                 if (child.nodeType == Node.TEXT_NODE || child.nodeType == Node.COMMENT_NODE) {
269                     output.append(child.nodeValue)
270                 } else {
271                     output.append(formatNode(child))
272                 }
273             }
274         }
275         output.append("</$parentName>")
276
277         return output.toString()
278     }
279 }