aboutsummaryrefslogtreecommitdiffstats
path: root/ms/blueprintsprocessor/functions/resource-resolution
diff options
context:
space:
mode:
authorAlexis de Talhouët <adetalhouet89@gmail.com>2019-06-29 00:28:40 -0400
committerAlexis de Talhouët <adetalhouet89@gmail.com>2019-07-04 15:53:42 -0400
commit5f0a5dde67bc0e7c99bd8f9e9b7c447e69ce62fa (patch)
tree0eb10b1fc86efa62c70114495b599d3701107bb1 /ms/blueprintsprocessor/functions/resource-resolution
parentc48ca03872b11a54cf898c93d4a62ef220f3c55f (diff)
Enforce resolutionKey or resourceId/resourceType
There are three existing ways to perform the resolution: either we don't store the results at all, whether for resource or template either we store using the resolution key. The combination of blueprintName, blueprintVersion, artifactName and resolutionKey has to be unique. If it is re-used, it is considered as a new attempt for that specific resolution request, and process will only try to resolve resources not marked at SUCCESS in the database. either we store using the resourceId and resourceType. As previous point, the combination of blueprintName, blueprintVersion, artifactName and resolutionKey has to be unique. If it is re-used, it is considered as a new attempt for that specific resolution request, and process will only try to resolve resources not marked at SUCCESS in the database. TBD: add uni tests Issue-ID: CCSDK-1423 Change-Id: I6b7198453cf0002edfa7a0c9ea3179555211b5dc Signed-off-by: Alexis de Talhouët <adetalhouet89@gmail.com>
Diffstat (limited to 'ms/blueprintsprocessor/functions/resource-resolution')
-rw-r--r--ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionComponent.kt52
-rw-r--r--ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionService.kt111
-rw-r--r--ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolution.kt57
-rw-r--r--ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionDBService.kt68
-rw-r--r--ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionRepository.kt15
-rwxr-xr-xms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolution.kt31
-rw-r--r--ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionRepository.kt35
-rw-r--r--ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionService.kt147
8 files changed, 393 insertions, 123 deletions
diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionComponent.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionComponent.kt
index 2039d2e5a..fd14cc8c1 100644
--- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionComponent.kt
+++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionComponent.kt
@@ -21,11 +21,13 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory
import org.onap.ccsdk.cds.blueprintsprocessor.core.api.data.ExecutionServiceInput
import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.utils.ResourceAssignmentUtils
import org.onap.ccsdk.cds.blueprintsprocessor.services.execution.AbstractComponentFunction
+import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
import org.onap.ccsdk.cds.controllerblueprints.core.asJsonNode
import org.onap.ccsdk.cds.controllerblueprints.core.asObjectNode
import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils
import org.springframework.beans.factory.config.ConfigurableBeanFactory
import org.springframework.context.annotation.Scope
+import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Component
@Component("component-resource-resolution")
@@ -35,41 +37,51 @@ open class ResourceResolutionComponent(private val resourceResolutionService: Re
override suspend fun processNB(executionRequest: ExecutionServiceInput) {
- val occurrence = getOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE)
- val resolutionKey = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY)
- val storeResult = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT)
- val resourceId = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID)
- val resourceType = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE)
-
+ val occurrence = getOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE).asInt()
+ val resolutionKey = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY)?.asText() ?: ""
+ val storeResult = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT)?.asBoolean() ?: false
+ val resourceId = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID)?.asText() ?: ""
+ val resourceType = getOptionalOperationInput(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE)?.asText() ?: ""
val properties: MutableMap<String, Any> = mutableMapOf()
- properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT] = storeResult?.asBoolean() ?: false
- properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY] = resolutionKey?.asText() ?: ""
- properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] = resourceId?.asText() ?: ""
- properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] = resourceType?.asText() ?: ""
+ properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT] = storeResult
+ properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY] = resolutionKey
+ properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] = resourceId
+ properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] = resourceType
+ properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE] = occurrence
+
+ val jsonResponse = JsonNodeFactory.instance.objectNode()
+
+ // validate inputs if we need to store the resource and template resolution.
+ if (storeResult) {
+ if (resolutionKey.isNotEmpty() && (resourceId.isNotEmpty() || resourceType.isNotEmpty())) {
+ throw BluePrintProcessorException("Can't proceed with the resolution: either provide resolution-key OR combination of resource-id and resource-type.")
+ } else if ((resourceType.isNotEmpty() && resourceId.isEmpty()) || (resourceType.isEmpty() && resourceId.isNotEmpty())) {
+ throw BluePrintProcessorException("Can't proceed with the resolution: both resource-id and resource-type should be provided, one of them is missing.")
+ } else if (resourceType.isEmpty() && resourceId.isEmpty() && resolutionKey.isEmpty()) {
+ throw BluePrintProcessorException("Can't proceed with the resolution: can't persist resolution without a correlation key. " +
+ "Either provide a resolution-key OR combination of resource-id and resource-type OR set `storeResult` to false.")
+ }
+ }
val artifactPrefixNamesNode = getOperationInput(ResourceResolutionConstants.INPUT_ARTIFACT_PREFIX_NAMES)
val artifactPrefixNames = JacksonUtils.getListFromJsonNode(artifactPrefixNamesNode, String::class.java)
- val resourceAssignmentRuntimeService =
- ResourceAssignmentUtils.transformToRARuntimeService(bluePrintRuntimeService, artifactPrefixNames.toString())
-
- val jsonResponse = JsonNodeFactory.instance.objectNode()
- for (j in 1..occurrence.asInt()) {
- val key = resolutionKey?.asText() + "-" + j
- properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY] = key
+ for (j in 1..occurrence) {
+ properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE] = j
- val response = resourceResolutionService.resolveResources(resourceAssignmentRuntimeService,
+ val response = resourceResolutionService.resolveResources(bluePrintRuntimeService,
nodeTemplateName,
artifactPrefixNames,
properties)
// provide indexed result in output if we have multiple resolution
- if (occurrence.asInt() != 1) {
- jsonResponse.set(key, response.asJsonNode())
+ if (occurrence != 1) {
+ jsonResponse.set(Integer.toString(j), response.asJsonNode())
} else {
jsonResponse.setAll(response.asObjectNode())
}
+
}
// Set Output Attributes
diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionService.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionService.kt
index 1270298a8..e08ac520a 100644
--- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionService.kt
+++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/ResourceResolutionService.kt
@@ -20,13 +20,14 @@ package org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
+import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.ResourceResolution
import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.ResourceResolutionDBService
import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db.TemplateResolutionService
import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.processor.ResourceAssignmentProcessor
import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.utils.ResourceAssignmentUtils
import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants
import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
-import org.onap.ccsdk.cds.controllerblueprints.core.asJsonType
+import org.onap.ccsdk.cds.controllerblueprints.core.asJsonPrimitive
import org.onap.ccsdk.cds.controllerblueprints.core.checkNotEmpty
import org.onap.ccsdk.cds.controllerblueprints.core.service.BluePrintRuntimeService
import org.onap.ccsdk.cds.controllerblueprints.core.service.BluePrintTemplateService
@@ -60,7 +61,7 @@ interface ResourceResolutionService {
@Service(ResourceResolutionConstants.SERVICE_RESOURCE_RESOLUTION)
open class ResourceResolutionServiceImpl(private var applicationContext: ApplicationContext,
- private var resolutionResultService: TemplateResolutionService,
+ private var templateResolutionDBService: TemplateResolutionService,
private var blueprintTemplateService: BluePrintTemplateService,
private var resourceResolutionDBService: ResourceResolutionDBService) :
ResourceResolutionService {
@@ -76,22 +77,30 @@ open class ResourceResolutionServiceImpl(private var applicationContext: Applica
override suspend fun resolveFromDatabase(bluePrintRuntimeService: BluePrintRuntimeService<*>,
artifactTemplate: String,
resolutionKey: String): String {
- return resolutionResultService.read(bluePrintRuntimeService, artifactTemplate, resolutionKey)
+ return templateResolutionDBService.findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactName(
+ bluePrintRuntimeService,
+ artifactTemplate,
+ resolutionKey)
}
override suspend fun resolveResources(bluePrintRuntimeService: BluePrintRuntimeService<*>, nodeTemplateName: String,
artifactNames: List<String>,
properties: Map<String, Any>): MutableMap<String, String> {
+
+ val resourceAssignmentRuntimeService =
+ ResourceAssignmentUtils.transformToRARuntimeService(bluePrintRuntimeService, artifactNames.toString())
+
val resolvedParams: MutableMap<String, String> = hashMapOf()
artifactNames.forEach { artifactName ->
- val resolvedContent = resolveResources(bluePrintRuntimeService, nodeTemplateName,
+ val resolvedContent = resolveResources(resourceAssignmentRuntimeService, nodeTemplateName,
artifactName, properties)
resolvedParams[artifactName] = resolvedContent
}
return resolvedParams
}
+
override suspend fun resolveResources(bluePrintRuntimeService: BluePrintRuntimeService<*>, nodeTemplateName: String,
artifactPrefix: String, properties: Map<String, Any>): String {
@@ -111,6 +120,13 @@ open class ResourceResolutionServiceImpl(private var applicationContext: Applica
as? MutableList<ResourceAssignment>
?: throw BluePrintProcessorException("couldn't get Dictionary Definitions")
+ if (isToStore(properties)) {
+ val existingResourceResolution = isNewResolution(bluePrintRuntimeService, properties, artifactPrefix)
+ if (existingResourceResolution.isNotEmpty()) {
+ updateResourceAssignmentWithExisting(existingResourceResolution, resourceAssignments)
+ }
+ }
+
// Get the Resource Dictionary Name
val resourceDefinitions: MutableMap<String, ResourceDefinition> = ResourceAssignmentUtils
.resourceDefinitions(bluePrintRuntimeService.bluePrintContext().rootPath)
@@ -128,10 +144,9 @@ open class ResourceResolutionServiceImpl(private var applicationContext: Applica
resolvedContent = blueprintTemplateService.generateContent(bluePrintRuntimeService, nodeTemplateName,
artifactTemplate, resolvedParamJsonContent)
- if (properties.containsKey(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT)
- && properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT] as Boolean) {
- resolutionResultService.write(properties, resolvedContent, bluePrintRuntimeService, artifactPrefix)
- log.info("template resolution saved into database successfully : ($properties)")
+ if (isToStore(properties)) {
+ templateResolutionDBService.write(properties, resolvedContent, bluePrintRuntimeService, artifactPrefix)
+ log.info("Template resolution saved into database successfully : ($properties)")
}
return resolvedContent
@@ -154,7 +169,9 @@ open class ResourceResolutionServiceImpl(private var applicationContext: Applica
coroutineScope {
bulkSequenced.forEach { batchResourceAssignments ->
// Execute Non Dependent Assignments in parallel ( ie asynchronously )
- val deferred = batchResourceAssignments.filter { it.name != "*" && it.name != "start" }
+ val deferred = batchResourceAssignments
+ .filter { it.name != "*" && it.name != "start" }
+ .filter { it.status != BluePrintConstants.STATUS_SUCCESS }
.map { resourceAssignment ->
async {
val dictionaryName = resourceAssignment.dictionaryName
@@ -197,12 +214,6 @@ open class ResourceResolutionServiceImpl(private var applicationContext: Applica
}
- private fun isToStore(properties: Map<String, Any>): Boolean {
- return properties.containsKey(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT)
- && properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT] as Boolean
- }
-
-
/**
* If the Source instance is "input", then it is not mandatory to have source Resource Definition, So it can
* derive the default input processor.
@@ -233,4 +244,74 @@ open class ResourceResolutionServiceImpl(private var applicationContext: Applica
return processorName
}
+
+ // Check whether to store or not the resolution of resource and template
+ private fun isToStore(properties: Map<String, Any>): Boolean {
+ return properties.containsKey(ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT)
+ && properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_STORE_RESULT] as Boolean
+ }
+
+ // Check whether resolution already exist in the database for the specified resolution-key or resourceId/resourceType
+ private suspend fun isNewResolution(bluePrintRuntimeService: BluePrintRuntimeService<*>,
+ properties: Map<String, Any>,
+ artifactPrefix: String): List<ResourceResolution> {
+ val occurrence = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE] as Int
+ val resolutionKey = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY] as String
+ val resourceId = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] as String
+ val resourceType = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] as String
+
+ if (resolutionKey.isNotEmpty()) {
+ val existingResourceAssignments =
+ resourceResolutionDBService.findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResolutionKeyAndOccurrence(
+ bluePrintRuntimeService,
+ resolutionKey,
+ occurrence,
+ artifactPrefix)
+ if (existingResourceAssignments.isNotEmpty()) {
+ log.info("Resolution with resolutionKey=($resolutionKey) already exist - will resolve all resources not already resolved.",
+ resolutionKey)
+ }
+ return existingResourceAssignments
+ } else if (resourceId.isNotEmpty() && resourceType.isNotEmpty()) {
+ val existingResourceAssignments =
+ resourceResolutionDBService.findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResourceIdAndResourceTypeAndOccurrence(
+ bluePrintRuntimeService,
+ resourceId,
+ resourceType,
+
+ occurrence,
+ artifactPrefix)
+ if (existingResourceAssignments.isNotEmpty()) {
+ log.info("Resolution with resourceId=($resourceId) and resourceType=($resourceType) already exist - will resolve " +
+ "all resources not already resolved.")
+ }
+ return existingResourceAssignments
+ }
+ return emptyList()
+ }
+
+ // Update the resource assignment list with the status of the resource that have already been resolved
+ private fun updateResourceAssignmentWithExisting(resourceResolutionList: List<ResourceResolution>,
+ resourceAssignmentList: MutableList<ResourceAssignment>) {
+ resourceResolutionList.forEach { resourceResolution ->
+ if (resourceResolution.status == BluePrintConstants.STATUS_SUCCESS) {
+ resourceAssignmentList.forEach {
+ if (compareOne(resourceResolution, it)) {
+ log.info("Resource ({}) already resolve: value=({})", it.name, resourceResolution.value)
+ it.property!!.value = resourceResolution.value!!.asJsonPrimitive()
+ it.status = resourceResolution.status
+ }
+ }
+ }
+ }
+ }
+
+ // Comparision between what we have in the database vs what we have to assign.
+ private fun compareOne(resourceResolution: ResourceResolution, resourceAssignment: ResourceAssignment): Boolean {
+ return (resourceResolution.name == resourceAssignment.name
+ && resourceResolution.dictionaryName == resourceAssignment.dictionaryName
+ && resourceResolution.dictionarySource == resourceAssignment.dictionarySource
+ && resourceResolution.dictionaryVersion == resourceAssignment.version)
+ }
+
}
diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolution.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolution.kt
index fa922cde5..baabfd913 100644
--- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolution.kt
+++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolution.kt
@@ -24,6 +24,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.io.Serializable
import java.util.*
import javax.persistence.Column
+import javax.persistence.ElementCollection
import javax.persistence.Entity
import javax.persistence.EntityListeners
import javax.persistence.Id
@@ -38,23 +39,6 @@ import javax.persistence.TemporalType
@Proxy(lazy = false)
class ResourceResolution : Serializable {
- @Id
- @Column(name = "resource_resolution_id")
- var id: String? = null
-
- @get:ApiModelProperty(value = "Resolution Key uniquely identifying the resolution of a given artifact within a CBA.",
- required = true)
- @Column(name = "resolution_key", nullable = false)
- var resolutionKey: String? = null
-
- @get:ApiModelProperty(value = "Resolution type.", required = true, example = "ServiceInstance, VfModule, VNF")
- @Column(name = "resource_type", nullable = false)
- var resourceType: String? = null
-
- @get:ApiModelProperty(value = "ID associated with the resolution type in the inventory system.", required = true)
- @Column(name = "resource_id", nullable = false)
- var resourceId: String? = null
-
@get:ApiModelProperty(value = "Name of the CBA.", required = true)
@Column(name = "blueprint_name", nullable = false)
var blueprintName: String? = null
@@ -67,19 +51,43 @@ class ResourceResolution : Serializable {
@Column(name = "artifact_name", nullable = false)
var artifactName: String? = null
+ @get:ApiModelProperty(value = "Name of the resource.", required = true)
+ @Column(name = "name", nullable = false)
+ var name: String? = null
+
+ @get:ApiModelProperty(value = "Value of the resolution.", required = true)
+ @Lob
+ @Column(name = "value", nullable = false)
+ var value: String? = null
+
@get:ApiModelProperty(value = "Whether success of failure.", required = true)
@Column(name = "status", nullable = false)
var status: String? = null
- @get:ApiModelProperty(value = "Name of the resource.", required = true)
- @Column(name = "name", nullable = false)
- var name: String? = null
+ @get:ApiModelProperty(value = "Resolution Key uniquely identifying the resolution of a given artifact within a CBA.",
+ required = true)
+ @Column(name = "resolution_key", nullable = false)
+ var resolutionKey: String? = null
+
+ @get:ApiModelProperty(value = "Resolution type.", required = true, example = "ServiceInstance, VfModule, VNF")
+ @Column(name = "resource_type", nullable = false)
+ var resourceType: String? = null
+
+ @get:ApiModelProperty(value = "ID associated with the resolution type in the inventory system.", required = true)
+ @Column(name = "resource_id", nullable = false)
+ var resourceId: String? = null
+
+ @get:ApiModelProperty(value = "If resolution occurred multiple time, this field provides the index.",
+ required = true)
+ @Column(name = "occurrence", nullable = false)
+ var occurrence: Int = 0
@get:ApiModelProperty(value = "Name of the data dictionary used for the resolution.", required = true)
@Column(name = "dictionary_name", nullable = false)
var dictionaryName: String? = null
- @get:ApiModelProperty(value = "Source associated with the data dictionary used for the resolution.", required = true)
+ @get:ApiModelProperty(value = "Source associated with the data dictionary used for the resolution.",
+ required = true)
@Column(name = "dictionary_status", nullable = false)
var dictionarySource: String? = null
@@ -87,10 +95,9 @@ class ResourceResolution : Serializable {
@Column(name = "dictionary_version", nullable = false)
var dictionaryVersion: Int = 0
- @get:ApiModelProperty(value = "Value of the resolution.", required = true)
- @Lob
- @Column(name = "value", nullable = false)
- var value: String? = null
+ @Id
+ @Column(name = "resource_resolution_id")
+ var id: String? = null
@get:ApiModelProperty(value = "Creation date of the resolution.", required = true)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionDBService.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionDBService.kt
index c6ffde742..e9679eeba 100644
--- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionDBService.kt
+++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionDBService.kt
@@ -25,6 +25,7 @@ import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils
import org.onap.ccsdk.cds.controllerblueprints.resource.dict.ResourceAssignment
import org.slf4j.LoggerFactory
import org.springframework.dao.DataIntegrityViolationException
+import org.springframework.dao.EmptyResultDataAccessException
import org.springframework.stereotype.Service
import java.util.*
@@ -33,6 +34,49 @@ class ResourceResolutionDBService(private val resourceResolutionRepository: Reso
private val log = LoggerFactory.getLogger(ResourceResolutionDBService::class.toString())
+ suspend fun findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResolutionKeyAndOccurrence(
+ bluePrintRuntimeService: BluePrintRuntimeService<*>, key: String,
+ occurrence: Int, artifactPrefix: String): List<ResourceResolution> {
+ return try {
+ val metadata = bluePrintRuntimeService.bluePrintContext().metadata!!
+
+ val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!!
+ val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!!
+
+ resourceResolutionRepository.findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResolutionKeyAndOccurrence(
+ blueprintName,
+ blueprintVersion,
+ artifactPrefix,
+ key,
+ occurrence)
+ } catch (e: EmptyResultDataAccessException) {
+ emptyList()
+ }
+ }
+
+ suspend fun findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResourceIdAndResourceTypeAndOccurrence(
+ bluePrintRuntimeService: BluePrintRuntimeService<*>, resourceId: String,
+ resourceType: String, occurrence: Int,
+ artifactPrefix: String): List<ResourceResolution> {
+ return try {
+
+ val metadata = bluePrintRuntimeService.bluePrintContext().metadata!!
+
+ val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!!
+ val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!!
+
+ resourceResolutionRepository.findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResourceIdAndResourceTypeAndOccurrence(
+ blueprintName,
+ blueprintVersion,
+ artifactPrefix,
+ resourceId,
+ resourceType,
+ occurrence)
+ } catch (e: EmptyResultDataAccessException) {
+ emptyList()
+ }
+ }
+
suspend fun readValue(blueprintName: String,
blueprintVersion: String,
artifactPrefix: String,
@@ -81,12 +125,11 @@ class ResourceResolutionDBService(private val resourceResolutionRepository: Reso
val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!!
val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!!
- val resolutionKey =
- properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY].toString()
- val resourceType =
- properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE].toString()
- val resourceId =
- properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID].toString()
+
+ val resolutionKey = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY] as String
+ val resourceId = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] as String
+ val resourceType = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] as String
+ val occurrence = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE] as Int
write(blueprintName,
blueprintVersion,
@@ -94,7 +137,8 @@ class ResourceResolutionDBService(private val resourceResolutionRepository: Reso
resourceId,
resourceType,
artifactPrefix,
- resourceAssignment)
+ resourceAssignment,
+ occurrence)
}
suspend fun write(blueprintName: String,
@@ -103,20 +147,22 @@ class ResourceResolutionDBService(private val resourceResolutionRepository: Reso
resourceId: String,
resourceType: String,
artifactPrefix: String,
- resourceAssignment: ResourceAssignment): ResourceResolution = withContext(Dispatchers.IO) {
+ resourceAssignment: ResourceAssignment,
+ occurrence: Int = 0): ResourceResolution = withContext(Dispatchers.IO) {
val resourceResolution = ResourceResolution()
resourceResolution.id = UUID.randomUUID().toString()
resourceResolution.artifactName = artifactPrefix
+ resourceResolution.occurrence = occurrence
resourceResolution.blueprintVersion = blueprintVersion
resourceResolution.blueprintName = blueprintName
resourceResolution.resolutionKey = resolutionKey
resourceResolution.resourceType = resourceType
resourceResolution.resourceId = resourceId
- if (BluePrintConstants.STATUS_FAILURE == resourceAssignment.status) {
- resourceResolution.value = ""
- } else {
+ if (BluePrintConstants.STATUS_SUCCESS == resourceAssignment.status) {
resourceResolution.value = JacksonUtils.getValue(resourceAssignment.property?.value!!).toString()
+ } else {
+ resourceResolution.value = ""
}
resourceResolution.name = resourceAssignment.name
resourceResolution.dictionaryName = resourceAssignment.dictionaryName
diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionRepository.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionRepository.kt
index d2cbf8411..429041e14 100644
--- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionRepository.kt
+++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/ResourceResolutionRepository.kt
@@ -34,4 +34,19 @@ interface ResourceResolutionRepository : JpaRepository<ResourceResolution, Strin
blueprintVersion: String,
resourceId: String,
resourceType: String): List<ResourceResolution>
+
+ fun findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResolutionKeyAndOccurrence(
+ blueprintName: String?,
+ blueprintVersion: String?,
+ artifactName: String,
+ resolutionKey: String,
+ occurrence: Int): List<ResourceResolution>
+
+ fun findByBlueprintNameAndBlueprintVersionAndArtifactNameAndResourceIdAndResourceTypeAndOccurrence(
+ blueprintName: String?,
+ blueprintVersion: String?,
+ artifactName: String,
+ resourceId: String,
+ resourceType: String,
+ occurrence: Int): List<ResourceResolution>
} \ No newline at end of file
diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolution.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolution.kt
index ae24a9ac2..70aadb4b1 100755
--- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolution.kt
+++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolution.kt
@@ -38,15 +38,6 @@ import javax.persistence.TemporalType
@Proxy(lazy = false)
class TemplateResolution : Serializable {
- @Id
- @Column(name = "template_resolution_id")
- var id: String? = null
-
- @get:ApiModelProperty(value = "Resolution Key uniquely identifying the resolution of a given artifact within a CBA.",
- required = true)
- @Column(name = "resolution_key", nullable = false)
- var resolutionKey: String? = null
-
@get:ApiModelProperty(value = "Name of the CBA.", required = true)
@Column(name = "blueprint_name", nullable = false)
var blueprintName: String? = null
@@ -64,6 +55,28 @@ class TemplateResolution : Serializable {
@Column(name = "result", nullable = false)
var result: String? = null
+ @get:ApiModelProperty(value = "Resolution Key uniquely identifying the resolution of a given artifact within a CBA.",
+ required = true)
+ @Column(name = "resolution_key", nullable = false)
+ var resolutionKey: String? = null
+
+ @get:ApiModelProperty(value = "Resolution type.", required = true, example = "ServiceInstance, VfModule, VNF")
+ @Column(name = "resource_type", nullable = false)
+ var resourceType: String? = null
+
+ @get:ApiModelProperty(value = "ID associated with the resolution type in the inventory system.", required = true)
+ @Column(name = "resource_id", nullable = false)
+ var resourceId: String? = null
+
+ @get:ApiModelProperty(value = "If resolution occurred multiple time, this field provides the index.",
+ required = true)
+ @Column(name = "occurrence", nullable = false)
+ var occurrence: Int = 0
+
+ @Id
+ @Column(name = "template_resolution_id")
+ var id: String? = null
+
@get:ApiModelProperty(value = "Creation date of the resolution.", required = true)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
@LastModifiedDate
diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionRepository.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionRepository.kt
index 136b5e952..440663f25 100644
--- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionRepository.kt
+++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionRepository.kt
@@ -16,10 +16,39 @@
package org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.db
import org.springframework.data.jpa.repository.JpaRepository
+import javax.transaction.Transactional
interface TemplateResolutionRepository : JpaRepository<TemplateResolution, String> {
- fun findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactName(key: String, blueprintName: String?,
- blueprintVersion: String?,
- artifactName: String): TemplateResolution
+ fun findByResourceIdAndResourceTypeAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence(
+ resourceId: String,
+ resourceType: String,
+ blueprintName: String?,
+ blueprintVersion: String?,
+ artifactName: String,
+ occurrence: Int): TemplateResolution ?
+
+ fun findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence(
+ key: String,
+ blueprintName: String?,
+ blueprintVersion: String?,
+ artifactName: String,
+ occurrence: Int): TemplateResolution ?
+
+ @Transactional
+ fun deleteByResourceIdAndResourceTypeAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence(
+ resourceId: String,
+ resourceType: String,
+ blueprintName: String?,
+ blueprintVersion: String?,
+ artifactName: String,
+ occurrence: Int)
+
+ @Transactional
+ fun deleteByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence(
+ key: String,
+ blueprintName: String?,
+ blueprintVersion: String?,
+ artifactName: String,
+ occurrence: Int)
}
diff --git a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionService.kt b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionService.kt
index c4e36401f..44662965d 100644
--- a/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionService.kt
+++ b/ms/blueprintsprocessor/functions/resource-resolution/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/functions/resource/resolution/db/TemplateResolutionService.kt
@@ -21,36 +21,65 @@ import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.Reso
import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants
import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintException
import org.onap.ccsdk.cds.controllerblueprints.core.service.BluePrintRuntimeService
+import org.slf4j.LoggerFactory
import org.springframework.dao.DataIntegrityViolationException
+import org.springframework.dao.EmptyResultDataAccessException
import org.springframework.stereotype.Service
import java.util.*
@Service
class TemplateResolutionService(private val templateResolutionRepository: TemplateResolutionRepository) {
- suspend fun read(bluePrintRuntimeService: BluePrintRuntimeService<*>,
- artifactPrefix: String,
- resolutionKey: String): String = withContext(Dispatchers.IO) {
+ private val log = LoggerFactory.getLogger(TemplateResolutionService::class.toString())
- val metadata = bluePrintRuntimeService.bluePrintContext().metadata!!
+ suspend fun findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactName(
+ bluePrintRuntimeService: BluePrintRuntimeService<*>,
+ artifactPrefix: String,
+ resolutionKey: String): String =
+ withContext(Dispatchers.IO) {
- val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!!
- val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!!
+ val metadata = bluePrintRuntimeService.bluePrintContext().metadata!!
- read(blueprintName, blueprintVersion, artifactPrefix, resolutionKey)
- }
+ val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!!
+ val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!!
- suspend fun read(blueprintName: String,
- blueprintVersion: String,
- artifactPrefix: String,
- resolutionKey: String): String = withContext(Dispatchers.IO) {
+ findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactName(blueprintName,
+ blueprintVersion,
+ artifactPrefix,
+ resolutionKey)
+ }
- templateResolutionRepository.findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactName(
- resolutionKey,
- blueprintName,
- blueprintVersion,
- artifactPrefix).result!!
- }
+ suspend fun findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactName(blueprintName: String,
+ blueprintVersion: String,
+ artifactPrefix: String,
+ resolutionKey: String,
+ occurrence: Int = 0): String =
+ withContext(Dispatchers.IO) {
+
+ templateResolutionRepository.findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence(
+ resolutionKey,
+ blueprintName,
+ blueprintVersion,
+ artifactPrefix,
+ occurrence)?.result ?: throw EmptyResultDataAccessException(1)
+ }
+
+ suspend fun findByResoureIdAndResourceTypeAndBlueprintNameAndBlueprintVersionAndArtifactName(blueprintName: String,
+ blueprintVersion: String,
+ artifactPrefix: String,
+ resourceId: String,
+ resourceType: String,
+ occurrence: Int = 0): String =
+ withContext(Dispatchers.IO) {
+
+ templateResolutionRepository.findByResourceIdAndResourceTypeAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence(
+ resourceId,
+ resourceType,
+ blueprintName,
+ blueprintVersion,
+ artifactPrefix,
+ occurrence)?.result!!
+ }
suspend fun write(properties: Map<String, Any>,
result: String, bluePrintRuntimeService: BluePrintRuntimeService<*>,
@@ -60,30 +89,68 @@ class TemplateResolutionService(private val templateResolutionRepository: Templa
val blueprintVersion = metadata[BluePrintConstants.METADATA_TEMPLATE_VERSION]!!
val blueprintName = metadata[BluePrintConstants.METADATA_TEMPLATE_NAME]!!
- val resolutionKey =
- properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY].toString()
+ val resolutionKey = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_KEY] as String
+ val resourceId = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_ID] as String
+ val resourceType = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_RESOURCE_TYPE] as String
+ val occurrence = properties[ResourceResolutionConstants.RESOURCE_RESOLUTION_INPUT_OCCURRENCE] as Int
- write(blueprintName, blueprintVersion, resolutionKey, artifactPrefix, result)
+ write(blueprintName,
+ blueprintVersion,
+ artifactPrefix,
+ result,
+ occurrence,
+ resolutionKey,
+ resourceId,
+ resourceType)
}
- suspend fun write(blueprintName: String,
- blueprintVersion: String,
- resolutionKey: String,
- artifactPrefix: String,
- template: String): TemplateResolution = withContext(Dispatchers.IO) {
-
- val resourceResolutionResult = TemplateResolution()
- resourceResolutionResult.id = UUID.randomUUID().toString()
- resourceResolutionResult.artifactName = artifactPrefix
- resourceResolutionResult.blueprintVersion = blueprintVersion
- resourceResolutionResult.blueprintName = blueprintName
- resourceResolutionResult.resolutionKey = resolutionKey
- resourceResolutionResult.result = template
-
- try {
- templateResolutionRepository.saveAndFlush(resourceResolutionResult)
- } catch (ex: DataIntegrityViolationException) {
- throw BluePrintException("Failed to store resource api result.", ex)
+ suspend fun write(blueprintName: String, blueprintVersion: String, artifactPrefix: String,
+ template: String, occurrence: Int = 0, resolutionKey: String = "", resourceId: String = "",
+ resourceType: String = ""): TemplateResolution =
+ withContext(Dispatchers.IO) {
+
+ val resourceResolutionResult = TemplateResolution()
+ resourceResolutionResult.id = UUID.randomUUID().toString()
+ resourceResolutionResult.artifactName = artifactPrefix
+ resourceResolutionResult.blueprintVersion = blueprintVersion
+ resourceResolutionResult.blueprintName = blueprintName
+ resourceResolutionResult.resolutionKey = resolutionKey
+ resourceResolutionResult.resourceId = resourceId
+ resourceResolutionResult.resourceType = resourceType
+ resourceResolutionResult.result = template
+ resourceResolutionResult.occurrence = occurrence
+
+ // Overwrite template resolution-key of resourceId/resourceType already existant
+ if (resolutionKey.isNotEmpty()) {
+ templateResolutionRepository.findByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence(
+ resolutionKey, blueprintName, blueprintVersion, artifactPrefix, occurrence)?.let {
+ log.info("Overwriting template resolution for blueprintName=($blueprintVersion), blueprintVersion=($blueprintName), " +
+ "artifactName=($artifactPrefix) and resolutionKey=($resolutionKey)")
+ templateResolutionRepository.deleteByResolutionKeyAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence(
+ resolutionKey,
+ blueprintName,
+ blueprintVersion,
+ artifactPrefix,
+ occurrence)
+ }
+ } else if (resourceId.isNotEmpty() && resourceType.isNotEmpty()) {
+ templateResolutionRepository.findByResourceIdAndResourceTypeAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence(
+ resourceId, resourceType, blueprintName, blueprintVersion, artifactPrefix, occurrence)?.let {
+ log.info("Overwriting template resolution for blueprintName=($blueprintVersion), blueprintVersion=($blueprintName), " +
+ "artifactName=($artifactPrefix), resourceId=($resourceId) and resourceType=($resourceType)")
+ templateResolutionRepository.deleteByResourceIdAndResourceTypeAndBlueprintNameAndBlueprintVersionAndArtifactNameAndOccurrence(
+ resourceId,
+ resourceType,
+ blueprintName,
+ blueprintVersion,
+ artifactPrefix,
+ occurrence)
+ }
+ }
+ try {
+ templateResolutionRepository.saveAndFlush(resourceResolutionResult)
+ } catch (ex: DataIntegrityViolationException) {
+ throw BluePrintException("Failed to store resource api result.", ex)
+ }
}
- }
} \ No newline at end of file