Formatting Code base with ktlint
[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         private val log = LoggerFactory.getLogger(ComponentConfigSnapshotsExecutor::class.java)
60
61         // input fields names accepted by this executor
62         const val INPUT_OPERATION = "operation"
63         const val INPUT_RESOURCE_ID = "resource-id"
64         const val INPUT_RESOURCE_TYPE = "resource-type"
65         const val INPUT_RESOURCE_STATUS = "resource-status"
66         const val INPUT_RESOURCE_SNAPSHOT = "resource-snapshot"
67         const val INPUT_DIFF_CONTENT_TYPE = "diff-content-type"
68
69         const val OPERATION_FETCH = "fetch"
70         const val OPERATION_STORE = "store"
71         const val OPERATION_DIFF = "diff"
72
73         const val DIFF_JSON = "JSON"
74         const val DIFF_XML = "XML"
75
76         // output fields names (and values) populated by this executor.
77         const val OUTPUT_STATUS = "config-snapshot-status"
78         const val OUTPUT_MESSAGE = "config-snapshot-message"
79         const val OUTPUT_SNAPSHOT = "config-snapshot-value"
80
81         const val OUTPUT_STATUS_SUCCESS = "success"
82         const val OUTPUT_STATUS_ERROR = "error"
83     }
84
85     /**
86      * Main entry point for ComponentConfigSnapshotsExecutor
87      * Supports 3 operations : fetch, store or diff
88      */
89     override suspend fun processNB(executionRequest: ExecutionServiceInput) {
90
91         val operation = getOptionalOperationInput(INPUT_OPERATION)?.returnNullIfMissing()?.textValue() ?: ""
92         val contentType = getOptionalOperationInput(INPUT_DIFF_CONTENT_TYPE)?.returnNullIfMissing()?.textValue() ?: ""
93         val resourceId = getOptionalOperationInput(INPUT_RESOURCE_ID)?.returnNullIfMissing()?.textValue() ?: ""
94         val resourceType = getOptionalOperationInput(INPUT_RESOURCE_TYPE)?.returnNullIfMissing()?.textValue() ?: ""
95         val resourceStatus = getOptionalOperationInput(INPUT_RESOURCE_STATUS)?.returnNullIfMissing()?.textValue() ?: RUNNING.name
96         val snapshot = getOptionalOperationInput(INPUT_RESOURCE_SNAPSHOT)?.returnNullIfMissing()?.textValue() ?: ""
97         val status = ResourceConfigSnapshot.Status.valueOf(resourceStatus)
98
99         when (operation) {
100             OPERATION_FETCH -> fetchConfigurationSnapshot(resourceId, resourceType, status)
101             OPERATION_STORE -> storeConfigurationSnapshot(snapshot, resourceId, resourceType, status)
102             OPERATION_DIFF -> compareConfigurationSnapshot(resourceId, resourceType, contentType)
103
104             else -> setNodeOutputErrors(
105                 OUTPUT_STATUS_ERROR,
106                 "Operation parameter must be fetch, store or diff"
107             )
108         }
109     }
110
111     /**
112      * General error handling for the executor.
113      */
114     override suspend fun recoverNB(runtimeException: RuntimeException, executionRequest: ExecutionServiceInput) {
115         setNodeOutputErrors(OUTPUT_STATUS_ERROR, "Error : ${runtimeException.message}")
116     }
117
118     /**
119      * Fetch a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
120      */
121     private suspend fun fetchConfigurationSnapshot(
122         resourceId: String,
123         resourceType: String,
124         status: ResourceConfigSnapshot.Status = RUNNING
125     ) {
126         try {
127             val cfgSnapshotValue = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, status)
128             setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, cfgSnapshotValue)
129         } catch (er: NoSuchElementException) {
130             val message = "No Resource config snapshot identified by resourceId={$resourceId}, " +
131                     "resourceType={$resourceType} does not exists"
132             setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
133         }
134     }
135
136     /**
137      * Store a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
138      */
139     private suspend fun storeConfigurationSnapshot(
140         cfgSnapshotValue: String,
141         resourceId: String,
142         resourceType: String,
143         status: ResourceConfigSnapshot.Status = RUNNING
144     ) {
145         if (cfgSnapshotValue.isNotEmpty()) {
146             val cfgSnapshotSaved = cfgSnapshotService.write(cfgSnapshotValue, resourceId, resourceType, status)
147             setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, cfgSnapshotSaved.config_snapshot ?: "")
148         } else {
149             val message = "Could not store config snapshot identified by resourceId={$resourceId},resourceType={$resourceType} does not exists"
150             setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
151         }
152     }
153
154     /**
155      * Compare two configs (RUNNING vs CANDIDATE) for resource identified by ID/type, using the specified contentType
156      */
157     private suspend fun compareConfigurationSnapshot(resourceId: String, resourceType: String, contentType: String) {
158
159         val cfgRunning = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, RUNNING)
160         val cfgCandidate = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, CANDIDATE)
161
162         if (cfgRunning.isEmpty() || cfgCandidate.isEmpty()) {
163             setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, Strings.EMPTY)
164             return
165         }
166
167         when (contentType.toUpperCase()) {
168             DIFF_JSON -> {
169                 val patchNode = JsonDiff.asJson(cfgRunning.jsonAsJsonType(), cfgCandidate.jsonAsJsonType())
170                 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, patchNode.toString())
171             }
172             DIFF_XML -> {
173                 val myDiff = DiffBuilder
174                     .compare(Input.fromString(cfgRunning))
175                     .withTest(Input.fromString(cfgCandidate))
176                     .checkForSimilar()
177                     .ignoreComments()
178                     .ignoreWhitespace()
179                     .normalizeWhitespace()
180                     .build()
181
182                 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, formatXmlDifferences(myDiff))
183             }
184             else -> {
185                 val message = "Could not compare config snapshots for type $contentType"
186                 setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
187             }
188         }
189     }
190
191     /**
192      * Utility function to set the output properties of the executor node
193      */
194     private fun setNodeOutputProperties(status: String, snapshot: String) {
195         setAttribute(OUTPUT_STATUS, status.asJsonPrimitive())
196         setAttribute(OUTPUT_SNAPSHOT, snapshot.asJsonPrimitive())
197         log.info("Setting output $OUTPUT_STATUS=$status, $OUTPUT_SNAPSHOT=$snapshot ")
198     }
199
200     /**
201      * Utility function to set the output properties and errors of the executor node, in case of errors
202      */
203     private fun setNodeOutputErrors(status: String, message: String) {
204         setAttribute(OUTPUT_STATUS, status.asJsonPrimitive())
205         setAttribute(OUTPUT_MESSAGE, message.asJsonPrimitive())
206         setAttribute(OUTPUT_SNAPSHOT, "".asJsonPrimitive())
207
208         log.info("Setting error and output $OUTPUT_STATUS=$status, $OUTPUT_MESSAGE=$message ")
209
210         addError(status, OUTPUT_MESSAGE, message)
211     }
212
213     /**
214      * Formats XmlUnit differences into xml-patch like response (RFC5261)
215      */
216     private fun formatXmlDifferences(differences: Diff): String {
217         val output = StringBuilder()
218         output.append(
219             "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
220                     "<diff>"
221         )
222         val diffIterator = differences.getDifferences().iterator()
223         while (diffIterator.hasNext()) {
224
225             val aDiff = diffIterator.next().comparison
226             when (aDiff.type) {
227                 ComparisonType.ATTR_VALUE -> {
228                     output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
229                         .append(aDiff.testDetails.value)
230                         .append("</replace>")
231                 }
232                 ComparisonType.TEXT_VALUE -> {
233                     output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
234                         .append(aDiff.testDetails.value)
235                         .append("</replace>")
236                 }
237                 ComparisonType.CHILD_LOOKUP -> {
238                     output.append("<add sel=\"").append(aDiff.testDetails.parentXPath).append("\">")
239                         .append(formatNode(aDiff.testDetails.target))
240                         .append("</add>")
241                 }
242                 ComparisonType.CHILD_NODELIST_LENGTH -> {
243                     // Ignored; will be processed in the CHILD_LOOKUP case
244                 }
245                 else -> {
246                     log.warn("Unsupported XML difference found: $aDiff")
247                 }
248             }
249         }
250         output.append("</diff>")
251
252         return output.toString()
253     }
254
255     /**
256      * Formats a node value obtained from an XmlUnit differences node
257      */
258     private fun formatNode(node: Node): String {
259         val output = StringBuilder()
260
261         val parentName = node.localName
262         output.append("<$parentName>")
263         if (node.hasChildNodes()) {
264             val nodes = node.childNodes
265             for (index in 1..nodes.length) {
266                 val child = nodes.item(index - 1)
267                 if (child.nodeType == Node.TEXT_NODE || child.nodeType == Node.COMMENT_NODE) {
268                     output.append(child.nodeValue)
269                 } else {
270                     output.append(formatNode(child))
271                 }
272             }
273         }
274         output.append("</$parentName>")
275
276         return output.toString()
277     }
278 }