Refactoring Netconf NetconfMessageUtils
[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 /**
43  * ComponentConfigSnapshotsExecutor
44  *
45  * Component that retrieves the saved configuration snapshot as identified by the input parameters,
46  * named resource-id and resource-type.
47  *
48  * It reports the content of the requested snapshot via properties, config-snapshot-status
49  * and config-snapshot-value.  In case of error, details can be found in the config-snapshot-status
50  * and config-snapshot-message properties
51  *
52  * @author Serge Simard
53  */
54 @Component("component-config-snapshots-executor")
55 @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
56 open class ComponentConfigSnapshotsExecutor(private val cfgSnapshotService: ResourceConfigSnapshotService) :
57         AbstractComponentFunction() {
58
59     companion object {
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 OUTPUT_STATUS = "config-snapshot-status"
79         const val OUTPUT_MESSAGE = "config-snapshot-message"
80         const val OUTPUT_SNAPSHOT = "config-snapshot-value"
81
82         const val OUTPUT_STATUS_SUCCESS = "success"
83         const val OUTPUT_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(OUTPUT_STATUS_ERROR,
106                                 "Operation parameter must be fetch, store or diff")
107         }
108     }
109
110     /**
111      * General error handling for the executor.
112      */
113     override suspend fun recoverNB(runtimeException: RuntimeException, executionRequest: ExecutionServiceInput) {
114         setNodeOutputErrors(OUTPUT_STATUS_ERROR, "Error : ${runtimeException.message}")
115     }
116
117     /**
118      * Fetch a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
119      */
120     private suspend fun fetchConfigurationSnapshot(resourceId: String, resourceType: String,
121                                                    status : ResourceConfigSnapshot.Status = RUNNING) {
122         try {
123             val cfgSnapshotValue = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, status)
124             setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, cfgSnapshotValue)
125         } catch (er : NoSuchElementException) {
126             val message = "No Resource config snapshot identified by resourceId={$resourceId}, " +
127                     "resourceType={$resourceType} does not exists"
128             setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
129         }
130     }
131
132     /**
133      * Store a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
134      */
135     private suspend fun storeConfigurationSnapshot(cfgSnapshotValue : String, resourceId: String, resourceType: String,
136                                                    status : ResourceConfigSnapshot.Status = RUNNING) {
137         if (cfgSnapshotValue.isNotEmpty()) {
138             val cfgSnapshotSaved = cfgSnapshotService.write(cfgSnapshotValue, resourceId, resourceType, status)
139             setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, cfgSnapshotSaved.config_snapshot ?: "" )
140         } else {
141             val message = "Could not store config snapshot identified by resourceId={$resourceId},resourceType={$resourceType} does not exists"
142             setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
143         }
144     }
145
146     /**
147      * Compare two configs (RUNNING vs CANDIDATE) for resource identified by ID/type, using the specified contentType
148      */
149     private suspend fun compareConfigurationSnapshot(resourceId: String, resourceType: String, contentType : String) {
150
151         val cfgRunning = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, RUNNING)
152         val cfgCandidate = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, CANDIDATE)
153
154         if (cfgRunning.isEmpty() || cfgCandidate.isEmpty()) {
155             setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, Strings.EMPTY)
156             return
157         }
158
159         when (contentType.toUpperCase()) {
160             DIFF_JSON -> {
161                 val patchNode = JsonDiff.asJson(cfgRunning.jsonAsJsonType(), cfgCandidate.jsonAsJsonType())
162                 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, patchNode.toString())
163             }
164             DIFF_XML -> {
165                 val myDiff = DiffBuilder
166                         .compare(Input.fromString(cfgRunning))
167                         .withTest(Input.fromString(cfgCandidate))
168                         .checkForSimilar()
169                         .ignoreComments()
170                         .ignoreWhitespace()
171                         .normalizeWhitespace()
172                         .build()
173
174                 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, formatXmlDifferences(myDiff))
175             }
176             else -> {
177                 val message = "Could not compare config snapshots for type $contentType"
178                 setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
179             }
180         }
181     }
182
183     /**
184      * Utility function to set the output properties of the executor node
185      */
186     private fun setNodeOutputProperties(status: String, snapshot: String) {
187         setAttribute(OUTPUT_STATUS, status.asJsonPrimitive())
188         setAttribute(OUTPUT_SNAPSHOT, snapshot.asJsonPrimitive())
189         log.info("Setting output $OUTPUT_STATUS=$status, $OUTPUT_SNAPSHOT=$snapshot ")
190     }
191
192     /**
193      * Utility function to set the output properties and errors of the executor node, in case of errors
194      */
195     private fun setNodeOutputErrors(status: String, message: String) {
196         setAttribute(OUTPUT_STATUS, status.asJsonPrimitive())
197         setAttribute(OUTPUT_MESSAGE, message.asJsonPrimitive())
198         setAttribute(OUTPUT_SNAPSHOT, "".asJsonPrimitive())
199
200         log.info("Setting error and output $OUTPUT_STATUS=$status, $OUTPUT_MESSAGE=$message ")
201
202         addError(status, OUTPUT_MESSAGE, message)
203     }
204
205     /**
206      * Formats XmlUnit differences into xml-patch like response (RFC5261)
207      */
208     private fun formatXmlDifferences(differences : Diff) : String {
209         val output = StringBuilder()
210         output.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
211                 "<diff>")
212         val diffIterator = differences.getDifferences().iterator()
213         while (diffIterator.hasNext()) {
214
215             val aDiff = diffIterator.next().comparison
216             when (aDiff.type) {
217                 ComparisonType.ATTR_VALUE -> {
218                     output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
219                             .append(aDiff.testDetails.value)
220                             .append("</replace>")
221                 }
222                 ComparisonType.TEXT_VALUE -> {
223                     output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
224                             .append(aDiff.testDetails.value)
225                             .append("</replace>")
226                 }
227                 ComparisonType.CHILD_LOOKUP -> {
228                     output.append("<add sel=\"").append(aDiff.testDetails.parentXPath).append("\">")
229                             .append(formatNode(aDiff.testDetails.target))
230                             .append("</add>")
231                 }
232                 ComparisonType.CHILD_NODELIST_LENGTH -> {
233                     // Ignored; will be processed in the CHILD_LOOKUP case
234                 }
235                 else -> {
236                     log.warn("Unsupported XML difference found: $aDiff")
237                 }
238             }
239         }
240         output.append("</diff>")
241
242         return output.toString()
243     }
244
245     /**
246      * Formats a node value obtained from an XmlUnit differences node
247      */
248     private fun formatNode(node: Node): String {
249         val output = StringBuilder()
250
251         val parentName = node.localName
252         output.append("<$parentName>")
253         if (node.hasChildNodes()) {
254             val nodes = node.childNodes
255             for (index in 1..nodes.length) {
256                 val child = nodes.item(index-1)
257                 if (child.nodeType == Node.TEXT_NODE || child.nodeType == Node.COMMENT_NODE) {
258                     output.append(child.nodeValue)
259                 } else {
260                     output.append(formatNode(child))
261                 }
262             }
263         }
264         output.append("</$parentName>")
265
266         return output.toString()
267     }
268 }