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.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
43 * ComponentConfigSnapshotsExecutor
45 * Component that retrieves the saved configuration snapshot as identified by the input parameters,
46 * named resource-id and resource-type.
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
52 * @author Serge Simard
54 @Component("component-config-snapshots-executor")
55 @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
56 open class ComponentConfigSnapshotsExecutor(private val cfgSnapshotService: ResourceConfigSnapshotService) :
57 AbstractComponentFunction() {
60 private val log = LoggerFactory.getLogger(ComponentConfigSnapshotsExecutor::class.java)
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"
70 const val OPERATION_FETCH = "fetch"
71 const val OPERATION_STORE = "store"
72 const val OPERATION_DIFF = "diff"
74 const val DIFF_JSON = "JSON"
75 const val DIFF_XML = "XML"
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"
82 const val OUTPUT_STATUS_SUCCESS = "success"
83 const val OUTPUT_STATUS_ERROR = "error"
87 * Main entry point for ComponentConfigSnapshotsExecutor
88 * Supports 3 operations : fetch, store or diff
90 override suspend fun processNB(executionRequest: ExecutionServiceInput) {
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)
101 OPERATION_FETCH -> fetchConfigurationSnapshot(resourceId, resourceType, status)
102 OPERATION_STORE -> storeConfigurationSnapshot(snapshot, resourceId, resourceType, status)
103 OPERATION_DIFF -> compareConfigurationSnapshot(resourceId, resourceType, contentType)
105 else -> setNodeOutputErrors(OUTPUT_STATUS_ERROR,
106 "Operation parameter must be fetch, store or diff")
111 * General error handling for the executor.
113 override suspend fun recoverNB(runtimeException: RuntimeException, executionRequest: ExecutionServiceInput) {
114 setNodeOutputErrors(OUTPUT_STATUS_ERROR, "Error : ${runtimeException.message}")
118 * Fetch a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
120 private suspend fun fetchConfigurationSnapshot(resourceId: String, resourceType: String,
121 status : ResourceConfigSnapshot.Status = RUNNING) {
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)
133 * Store a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
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 ?: "" )
141 val message = "Could not store config snapshot identified by resourceId={$resourceId},resourceType={$resourceType} does not exists"
142 setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
147 * Compare two configs (RUNNING vs CANDIDATE) for resource identified by ID/type, using the specified contentType
149 private suspend fun compareConfigurationSnapshot(resourceId: String, resourceType: String, contentType : String) {
151 val cfgRunning = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, RUNNING)
152 val cfgCandidate = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, CANDIDATE)
154 if (cfgRunning.isEmpty() || cfgCandidate.isEmpty()) {
155 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, Strings.EMPTY)
159 when (contentType.toUpperCase()) {
161 val patchNode = JsonDiff.asJson(cfgRunning.jsonAsJsonType(), cfgCandidate.jsonAsJsonType())
162 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, patchNode.toString())
165 val myDiff = DiffBuilder
166 .compare(Input.fromString(cfgRunning))
167 .withTest(Input.fromString(cfgCandidate))
171 .normalizeWhitespace()
174 setNodeOutputProperties(OUTPUT_STATUS_SUCCESS, formatXmlDifferences(myDiff))
177 val message = "Could not compare config snapshots for type $contentType"
178 setNodeOutputErrors(OUTPUT_STATUS_ERROR, message)
184 * Utility function to set the output properties of the executor node
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 ")
193 * Utility function to set the output properties and errors of the executor node, in case of errors
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())
200 log.info("Setting error and output $OUTPUT_STATUS=$status, $OUTPUT_MESSAGE=$message ")
202 addError(status, OUTPUT_MESSAGE, message)
206 * Formats XmlUnit differences into xml-patch like response (RFC5261)
208 private fun formatXmlDifferences(differences : Diff) : String {
209 val output = StringBuilder()
210 output.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
212 val diffIterator = differences.getDifferences().iterator()
213 while (diffIterator.hasNext()) {
215 val aDiff = diffIterator.next().comparison
217 ComparisonType.ATTR_VALUE -> {
218 output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
219 .append(aDiff.testDetails.value)
220 .append("</replace>")
222 ComparisonType.TEXT_VALUE -> {
223 output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
224 .append(aDiff.testDetails.value)
225 .append("</replace>")
227 ComparisonType.CHILD_LOOKUP -> {
228 output.append("<add sel=\"").append(aDiff.testDetails.parentXPath).append("\">")
229 .append(formatNode(aDiff.testDetails.target))
232 ComparisonType.CHILD_NODELIST_LENGTH -> {
233 // Ignored; will be processed in the CHILD_LOOKUP case
236 log.warn("Unsupported XML difference found: $aDiff")
240 output.append("</diff>")
242 return output.toString()
246 * Formats a node value obtained from an XmlUnit differences node
248 private fun formatNode(node: Node): String {
249 val output = StringBuilder()
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)
260 output.append(formatNode(child))
264 output.append("</$parentName>")
266 return output.toString()