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
42 * ComponentConfigSnapshotsExecutor
44 * Component that retrieves the saved configuration snapshot as identified by the input parameters,
45 * named resource-id and resource-type.
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
51 * @author Serge Simard
53 @Component("component-config-snapshots-executor")
54 @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
55 open class ComponentConfigSnapshotsExecutor(private val cfgSnapshotService: ResourceConfigSnapshotService) :
56 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 ATTRIBUTE_STATUS = "config-snapshot-status"
79 const val ATTRIBUTE_MESSAGE = "config-snapshot-message"
80 const val ATTRIBUTE_SNAPSHOT = "config-snapshot-value"
82 const val ATTRIBUTE_STATUS_SUCCESS = "success"
83 const val ATTRIBUTE_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(
106 ATTRIBUTE_STATUS_ERROR,
107 "Operation parameter must be fetch, store or diff"
113 * General error handling for the executor.
115 override suspend fun recoverNB(runtimeException: RuntimeException, executionRequest: ExecutionServiceInput) {
116 setNodeOutputErrors(ATTRIBUTE_STATUS_ERROR, "Error : ${runtimeException.message}")
120 * Fetch a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
122 private suspend fun fetchConfigurationSnapshot(
124 resourceType: String,
125 status: ResourceConfigSnapshot.Status = RUNNING
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)
138 * Store a configuration snapshot, for resource identified by ID/type, of type status (RUNNING by default)
140 private suspend fun storeConfigurationSnapshot(
141 cfgSnapshotValue: String,
143 resourceType: String,
144 status: ResourceConfigSnapshot.Status = RUNNING
146 if (cfgSnapshotValue.isNotEmpty()) {
147 val cfgSnapshotSaved = cfgSnapshotService.write(cfgSnapshotValue, resourceId, resourceType, status)
148 setNodeOutputProperties(ATTRIBUTE_STATUS_SUCCESS, cfgSnapshotSaved.config_snapshot ?: "")
150 val message = "Could not store config snapshot identified by resourceId={$resourceId},resourceType={$resourceType} does not exists"
151 setNodeOutputErrors(ATTRIBUTE_STATUS_ERROR, message)
156 * Compare two configs (RUNNING vs CANDIDATE) for resource identified by ID/type, using the specified contentType
158 private suspend fun compareConfigurationSnapshot(resourceId: String, resourceType: String, contentType: String) {
160 val cfgRunning = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, RUNNING)
161 val cfgCandidate = cfgSnapshotService.findByResourceIdAndResourceTypeAndStatus(resourceId, resourceType, CANDIDATE)
163 if (cfgRunning.isEmpty() || cfgCandidate.isEmpty()) {
164 setNodeOutputProperties(ATTRIBUTE_STATUS_SUCCESS, Strings.EMPTY)
168 when (contentType.toUpperCase()) {
170 val patchNode = JsonDiff.asJson(cfgRunning.jsonAsJsonType(), cfgCandidate.jsonAsJsonType())
171 setNodeOutputProperties(ATTRIBUTE_STATUS_SUCCESS, patchNode.toString())
174 val myDiff = DiffBuilder
175 .compare(Input.fromString(cfgRunning))
176 .withTest(Input.fromString(cfgCandidate))
180 .normalizeWhitespace()
183 setNodeOutputProperties(ATTRIBUTE_STATUS_SUCCESS, formatXmlDifferences(myDiff))
186 val message = "Could not compare config snapshots for type $contentType"
187 setNodeOutputErrors(ATTRIBUTE_STATUS_ERROR, message)
193 * Utility function to set the output properties of the executor node
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")
202 * Utility function to set the output properties and errors of the executor node, in case of errors
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())
209 log.info("Setting error and output $ATTRIBUTE_STATUS=$status, $ATTRIBUTE_MESSAGE=$message ")
211 addError(status, ATTRIBUTE_MESSAGE, message)
215 * Formats XmlUnit differences into xml-patch like response (RFC5261)
217 private fun formatXmlDifferences(differences: Diff): String {
218 val output = StringBuilder()
220 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
223 val diffIterator = differences.getDifferences().iterator()
224 while (diffIterator.hasNext()) {
226 val aDiff = diffIterator.next().comparison
228 ComparisonType.ATTR_VALUE -> {
229 output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
230 .append(aDiff.testDetails.value)
231 .append("</replace>")
233 ComparisonType.TEXT_VALUE -> {
234 output.append("<replace sel=\"").append(aDiff.testDetails.xPath).append("\">")
235 .append(aDiff.testDetails.value)
236 .append("</replace>")
238 ComparisonType.CHILD_LOOKUP -> {
239 output.append("<add sel=\"").append(aDiff.testDetails.parentXPath).append("\">")
240 .append(formatNode(aDiff.testDetails.target))
243 ComparisonType.CHILD_NODELIST_LENGTH -> {
244 // Ignored; will be processed in the CHILD_LOOKUP case
247 log.warn("Unsupported XML difference found: $aDiff")
251 output.append("</diff>")
253 return output.toString()
257 * Formats a node value obtained from an XmlUnit differences node
259 private fun formatNode(node: Node): String {
260 val output = StringBuilder()
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)
271 output.append(formatNode(child))
275 output.append("</$parentName>")
277 return output.toString()