2 * Copyright © 2019 Bell Canada.
3 * Modifications Copyright © 2018-2019 IBM.
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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 package org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots
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.RUNNING
25 import org.onap.ccsdk.cds.blueprintsprocessor.functions.config.snapshots.db.ResourceConfigSnapshot.Status.CANDIDATE
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.*
29 import org.slf4j.LoggerFactory
30 import org.springframework.beans.factory.config.ConfigurableBeanFactory
31 import org.springframework.context.annotation.Scope
32 import org.springframework.stereotype.Component
33 import org.w3c.dom.Node
34 import org.xmlunit.builder.DiffBuilder
35 import org.xmlunit.builder.Input
36 import org.xmlunit.diff.*
40 * ComponentConfigSnapshotsExecutor
42 * Component that retrieves the saved configuration snapshot as identified by the input parameters,
43 * named resource-id and resource-type.
45 * It reports the content of the requested snapshot via properties, config-snapshot-status
46 * and config-snapshot-value. In case of error, details can be found in the config-snapshot-status
47 * and config-snapshot-message properties
49 * @author Serge Simard
51 @Component("component-config-snapshots-executor")
52 @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
53 open class ComponentConfigSnapshotsExecutor(private val cfgSnapshotService: ResourceConfigSnapshotService) :
54 AbstractComponentFunction() {
57 private val log = LoggerFactory.getLogger(ComponentConfigSnapshotsExecutor::class.java)
59 // input fields names accepted by this executor
60 const val INPUT_OPERATION = "operation"
61 const val INPUT_RESOURCE_ID = "resource-id"
62 const val INPUT_RESOURCE_TYPE = "resource-type"
63 const val INPUT_RESOURCE_STATUS = "resource-status"
64 const val INPUT_RESOURCE_SNAPSHOT = "resource-snapshot"
65 const val INPUT_DIFF_CONTENT_TYPE = "diff-content-type"
67 const val OPERATION_FETCH = "fetch"
68 const val OPERATION_STORE = "store"
69 const val OPERATION_DIFF = "diff"
71 const val DIFF_JSON = "JSON"
72 const val DIFF_XML = "XML"
74 // output fields names (and values) populated by this executor.
75 const val OUTPUT_STATUS = "config-snapshot-status"
76 const val OUTPUT_MESSAGE = "config-snapshot-message"
77 const val OUTPUT_SNAPSHOT = "config-snapshot-value"
79 const val OUTPUT_STATUS_SUCCESS = "success"
80 const val OUTPUT_STATUS_ERROR = "error"
84 * Main entry point for ComponentConfigSnapshotsExecutor
85 * Supports 3 operations : fetch, store or diff
87 override suspend fun processNB(executionRequest: ExecutionServiceInput) {
89 val operation = getOptionalOperationInput(INPUT_OPERATION)?.returnNullIfMissing()?.textValue() ?: ""
90 val contentType = getOptionalOperationInput(INPUT_DIFF_CONTENT_TYPE)?.returnNullIfMissing()?.textValue() ?: ""
91 val resourceId = getOptionalOperationInput(INPUT_RESOURCE_ID)?.returnNullIfMissing()?.textValue() ?: ""
92 val resourceType = getOptionalOperationInput(INPUT_RESOURCE_TYPE)?.returnNullIfMissing()?.textValue() ?: ""
93 val resourceStatus = getOptionalOperationInput(INPUT_RESOURCE_STATUS)?.returnNullIfMissing()?.textValue() ?: RUNNING.name
94 val snapshot = getOptionalOperationInput(INPUT_RESOURCE_SNAPSHOT)?.returnNullIfMissing()?.textValue() ?: ""
95 val status = ResourceConfigSnapshot.Status.valueOf(resourceStatus)
98 OPERATION_FETCH -> fetchConfigurationSnapshot(resourceId, resourceType, status)
99 OPERATION_STORE -> storeConfigurationSnapshot(snapshot, resourceId, resourceType, status)
100 OPERATION_DIFF -> compareConfigurationSnapshot(resourceId, resourceType, contentType)
102 else -> setNodeOutputErrors(OUTPUT_STATUS_ERROR,
103 "Operation parameter must be fetch, store or diff")
108 * General error handling for the executor.
110 override suspend fun recoverNB(runtimeException: RuntimeException, executionRequest: ExecutionServiceInput) {
111 setNodeOutputErrors(OUTPUT_STATUS_ERROR, "Error : ${runtimeException.message}")
115 * Fetch a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
117 private suspend fun fetchConfigurationSnapshot(resourceId: String, resourceType: String,
118 status : ResourceConfigSnapshot.Status = RUNNING) {
120 val cfgSnapshotValue = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, status)
121 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, cfgSnapshotValue)
122 } catch (er : NoSuchElementException) {
123 val message = "No Resource config snapshot identified by resourceId={$resourceId}, " +
124 "resourceType={$resourceType} does not exists"
125 setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
130 * Store a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
132 private suspend fun storeConfigurationSnapshot(cfgSnapshotValue : String, resourceId: String, resourceType: String,
133 status : ResourceConfigSnapshot.Status = RUNNING) {
134 if (cfgSnapshotValue.isNotEmpty()) {
135 val cfgSnapshotSaved = cfgSnapshotService.write(cfgSnapshotValue, resourceId, resourceType, status)
136 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, cfgSnapshotSaved.config_snapshot ?: "" )
138 val message = "Could not store config snapshot identified by resourceId={$resourceId},resourceType={$resourceType} does not exists"
139 setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
144 * Compare two configs (RUNNING vs CANDIDATE) for resource identified by ID/type, using the specified contentType
146 private suspend fun compareConfigurationSnapshot(resourceId: String, resourceType: String, contentType : String) {
148 val cfgRunning = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, RUNNING)
149 val cfgCandidate = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, CANDIDATE)
151 if (cfgRunning.isEmpty() || cfgCandidate.isEmpty()) {
152 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, Strings.EMPTY)
156 when (contentType.toUpperCase()) {
158 val patchNode = JsonDiff.asJson(cfgRunning.jsonAsJsonType(), cfgCandidate.jsonAsJsonType())
159 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, patchNode.toString())
162 val myDiff = DiffBuilder
163 .compare(Input.fromString(cfgRunning))
164 .withTest(Input.fromString(cfgCandidate))
168 .normalizeWhitespace()
171 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, formatXmlDifferences(myDiff))
174 val message = "Could not compare config snapshots for type $contentType"
175 setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
181 * Utility function to set the output properties of the executor node
183 private fun setNodeOutputProperties(status: String, snapshot: String) {
184 setAttribute(OUTPUT_STATUS, status.asJsonPrimitive())
185 setAttribute(OUTPUT_SNAPSHOT, snapshot.asJsonPrimitive())
186 log.info("Setting output $OUTPUT_STATUS=$status, $OUTPUT_SNAPSHOT=$snapshot ")
190 * Utility function to set the output properties and errors of the executor node, in case of errors
192 private fun setNodeOutputErrors(status: String, message: String) {
193 setAttribute(OUTPUT_STATUS, status.asJsonPrimitive())
194 setAttribute(OUTPUT_MESSAGE, message.asJsonPrimitive())
195 setAttribute(OUTPUT_SNAPSHOT, "".asJsonPrimitive())
197 log.info("Setting error and output $OUTPUT_STATUS=$status, $OUTPUT_MESSAGE=$message ")
199 addError(status, OUTPUT_MESSAGE, message)
203 * Formats XmlUnit differences into xml-patch like response (RFC5261)
205 private fun formatXmlDifferences(differences : Diff) : String {
206 val output = StringBuilder()
207 output.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
209 val diffIterator = differences.getDifferences().iterator()
210 while (diffIterator.hasNext()) {
212 val aDiff = diffIterator.next().comparison
214 ComparisonType.ATTR_VALUE -> {
215 output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
216 .append(aDiff.testDetails.value)
217 .append("</replace>")
219 ComparisonType.TEXT_VALUE -> {
220 output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
221 .append(aDiff.testDetails.value)
222 .append("</replace>")
224 ComparisonType.CHILD_LOOKUP -> {
225 output.append("<add sel=\"").append(aDiff.testDetails.parentXPath).append("\">")
226 .append(formatNode(aDiff.testDetails.target))
229 ComparisonType.CHILD_NODELIST_LENGTH -> {
230 // Ignored; will be processed in the CHILD_LOOKUP case
233 log.warn("Unsupported XML difference found: $aDiff")
237 output.append("</diff>")
239 return output.toString()
243 * Formats a node value obtained from an XmlUnit differences node
245 private fun formatNode(node: Node): String {
246 val output = StringBuilder()
248 val parentName = node.localName
249 output.append("<$parentName>")
250 if (node.hasChildNodes()) {
251 val nodes = node.childNodes
252 for (index in 1..nodes.length) {
253 val child = nodes.item(index-1)
254 if (child.nodeType == Node.TEXT_NODE || child.nodeType == Node.COMMENT_NODE) {
255 output.append(child.nodeValue)
257 output.append(formatNode(child))
261 output.append("</$parentName>")
263 return output.toString()