2 * Copyright © 2017-2018 AT&T Intellectual Property.
3 * Modifications Copyright (c) 2019 IBM, Bell Canada.
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.resource.resolution.utils
20 import com.fasterxml.jackson.databind.JsonNode
21 import com.fasterxml.jackson.databind.ObjectMapper
22 import com.fasterxml.jackson.databind.node.ArrayNode
23 import com.fasterxml.jackson.databind.node.NullNode
24 import com.fasterxml.jackson.databind.node.ObjectNode
25 import com.fasterxml.jackson.databind.node.TextNode
26 import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceAssignmentRuntimeService
27 import org.onap.ccsdk.cds.blueprintsprocessor.functions.resource.resolution.ResourceResolutionConstants
28 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants
29 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
30 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintTypes
31 import org.onap.ccsdk.cds.controllerblueprints.core.asJsonType
32 import org.onap.ccsdk.cds.controllerblueprints.core.checkFileExists
33 import org.onap.ccsdk.cds.controllerblueprints.core.checkNotEmpty
34 import org.onap.ccsdk.cds.controllerblueprints.core.common.ApplicationConstants.LOG_REDACTED
35 import org.onap.ccsdk.cds.controllerblueprints.core.isComplexType
36 import org.onap.ccsdk.cds.controllerblueprints.core.isNotEmpty
37 import org.onap.ccsdk.cds.controllerblueprints.core.isNullOrMissing
38 import org.onap.ccsdk.cds.controllerblueprints.core.normalizedFile
39 import org.onap.ccsdk.cds.controllerblueprints.core.nullToEmpty
40 import org.onap.ccsdk.cds.controllerblueprints.core.rootFieldsToMap
41 import org.onap.ccsdk.cds.controllerblueprints.core.service.BluePrintRuntimeService
42 import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonReactorUtils
43 import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils
44 import org.onap.ccsdk.cds.controllerblueprints.core.utils.PropertyDefinitionUtils.Companion.hasLogProtect
45 import org.onap.ccsdk.cds.controllerblueprints.resource.dict.DictionaryMetadataEntry
46 import org.onap.ccsdk.cds.controllerblueprints.resource.dict.KeyIdentifier
47 import org.onap.ccsdk.cds.controllerblueprints.resource.dict.ResolutionSummary
48 import org.onap.ccsdk.cds.controllerblueprints.resource.dict.ResourceAssignment
49 import org.onap.ccsdk.cds.controllerblueprints.resource.dict.ResourceDefinition
50 import org.slf4j.LoggerFactory
53 class ResourceAssignmentUtils {
56 private val logger = LoggerFactory.getLogger(ResourceAssignmentUtils::class.toString())
58 suspend fun resourceDefinitions(blueprintBasePath: String): MutableMap<String, ResourceDefinition> {
59 val dictionaryFile = normalizedFile(
60 blueprintBasePath, BluePrintConstants.TOSCA_DEFINITIONS_DIR,
61 ResourceResolutionConstants.FILE_NAME_RESOURCE_DEFINITION_TYPES
63 checkFileExists(dictionaryFile) { "resource definition file(${dictionaryFile.absolutePath}) is missing" }
64 return JacksonReactorUtils.getMapFromFile(dictionaryFile, ResourceDefinition::class.java)
67 @Throws(BluePrintProcessorException::class)
68 fun setResourceDataValue(
69 resourceAssignment: ResourceAssignment,
70 raRuntimeService: ResourceAssignmentRuntimeService,
73 // TODO("See if Validation is needed in future with respect to conversion and Types")
74 return setResourceDataValue(resourceAssignment, raRuntimeService, value.asJsonType())
77 @Throws(BluePrintProcessorException::class)
78 fun setResourceDataValue(
79 resourceAssignment: ResourceAssignment,
80 raRuntimeService: ResourceAssignmentRuntimeService,
83 val resourceProp = checkNotNull(resourceAssignment.property) {
84 "Failed in setting resource value for resource mapping $resourceAssignment"
86 checkNotEmpty(resourceAssignment.name) {
87 "Failed in setting resource value for resource mapping $resourceAssignment"
90 if (resourceAssignment.dictionaryName.isNullOrEmpty()) {
91 resourceAssignment.dictionaryName = resourceAssignment.name
93 "Missing dictionary key, setting with template key (${resourceAssignment.name}) " +
94 "as dictionary key (${resourceAssignment.dictionaryName})"
99 if (resourceProp.type.isNotEmpty()) {
100 val metadata = resourceAssignment.property!!.metadata
101 val valueToPrint = getValueToLog(metadata, value)
103 "Setting Resource Value ($valueToPrint) for Resource Name " +
104 "(${resourceAssignment.name}), definition(${resourceAssignment.dictionaryName}) " +
105 "of type (${resourceProp.type})"
107 setResourceValue(resourceAssignment, raRuntimeService, value)
108 resourceAssignment.updatedDate = Date()
109 resourceAssignment.updatedBy = BluePrintConstants.USER_SYSTEM
110 resourceAssignment.status = BluePrintConstants.STATUS_SUCCESS
112 } catch (e: Exception) {
113 throw BluePrintProcessorException(
114 "Failed in setting value for template key " +
115 "(${resourceAssignment.name}) and dictionary key (${resourceAssignment.dictionaryName}) of " +
116 "type (${resourceProp.type}) with error message (${e.message})", e
121 private fun setResourceValue(
122 resourceAssignment: ResourceAssignment,
123 raRuntimeService: ResourceAssignmentRuntimeService,
126 // TODO("See if Validation is needed wrt to type before storing")
127 raRuntimeService.putResolutionStore(resourceAssignment.name, value)
128 raRuntimeService.putDictionaryStore(resourceAssignment.dictionaryName!!, value)
129 resourceAssignment.property!!.value = value
132 fun setFailedResourceDataValue(resourceAssignment: ResourceAssignment, message: String?) {
133 if (isNotEmpty(resourceAssignment.name)) {
134 resourceAssignment.updatedDate = Date()
135 resourceAssignment.updatedBy = BluePrintConstants.USER_SYSTEM
136 resourceAssignment.status = BluePrintConstants.STATUS_FAILURE
137 resourceAssignment.message = message
141 @Throws(BluePrintProcessorException::class)
142 fun assertTemplateKeyValueNotNull(resourceAssignment: ResourceAssignment) {
143 val resourceProp = checkNotNull(resourceAssignment.property) {
144 "Failed to populate mandatory resource resource mapping $resourceAssignment"
146 if (resourceProp.required != null && resourceProp.required!! && resourceProp.value.isNullOrMissing()) {
147 logger.error("failed to populate mandatory resource mapping ($resourceAssignment)")
148 throw BluePrintProcessorException("failed to populate mandatory resource mapping ($resourceAssignment)")
152 @Throws(BluePrintProcessorException::class)
153 fun generateResourceDataForAssignments(assignments: List<ResourceAssignment>): String {
156 val mapper = ObjectMapper()
157 val root: ObjectNode = mapper.createObjectNode()
159 var containsLogProtected = false
160 assignments.forEach {
161 if (isNotEmpty(it.name) && it.property != null) {
163 val metadata = it.property!!.metadata
164 val type = nullToEmpty(it.property?.type).toLowerCase()
165 val value = useDefaultValueIfNull(it, rName)
166 val valueToPrint = getValueToLog(metadata, value)
167 containsLogProtected = hasLogProtect(metadata)
168 logger.trace("Generating Resource name ($rName), type ($type), value ($valueToPrint)")
169 root.set<JsonNode>(rName, value)
172 result = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(root)
174 if (!containsLogProtected) {
175 logger.info("Generated Resource Param Data ($result)")
177 } catch (e: Exception) {
178 throw BluePrintProcessorException("Resource Assignment is failed with $e.message", e)
184 @Throws(BluePrintProcessorException::class)
185 fun generateResourceForAssignments(assignments: List<ResourceAssignment>): MutableMap<String, JsonNode> {
186 val data: MutableMap<String, JsonNode> = hashMapOf()
187 assignments.forEach {
188 if (isNotEmpty(it.name) && it.property != null) {
190 val metadata = it.property!!.metadata
191 val type = nullToEmpty(it.property?.type).toLowerCase()
192 val value = useDefaultValueIfNull(it, rName)
193 val valueToPrint = getValueToLog(metadata, value)
195 logger.trace("Generating Resource name ($rName), type ($type), value ($valueToPrint)")
202 fun generateResolutionSummaryData(
203 resourceAssignments: List<ResourceAssignment>,
204 resourceDefinitions: Map<String, ResourceDefinition>
206 val emptyTextNode = TextNode.valueOf("")
207 val resolutionSummaryList = resourceAssignments.map {
208 val definition = resourceDefinitions[it.name]
209 val description = definition?.property?.description ?: ""
210 val value = it.property?.value
211 ?.let { v -> if (v.isNullOrMissing()) emptyTextNode else v }
214 var payload: JsonNode = definition?.sources?.get(it.dictionarySource)
215 ?.properties?.get("resolved-payload")
216 ?.let { p -> if (p.isNullOrMissing()) emptyTextNode else p }
219 val metadata = definition?.property?.metadata
220 ?.map { e -> DictionaryMetadataEntry(e.key, e.value) }
221 ?.toMutableList() ?: mutableListOf()
223 val keyIdentifiers: MutableList<KeyIdentifier> = it.keyIdentifiers.map { k ->
224 if (k.value.isNullOrMissing()) KeyIdentifier(k.name, emptyTextNode) else k
230 it.property?.required ?: false,
231 it.property?.type ?: "",
235 it.dictionaryName ?: "",
236 it.dictionarySource ?: "",
242 // Wrapper needed for integration with SDNC
243 val data = mapOf("resolution-summary" to resolutionSummaryList)
244 return JacksonUtils.getJson(data, includeNull = true)
247 private fun useDefaultValueIfNull(
248 resourceAssignment: ResourceAssignment,
249 resourceAssignmentName: String
251 if (resourceAssignment.property?.value == null) {
252 val defaultValue = "\${$resourceAssignmentName}"
253 return TextNode(defaultValue)
255 return resourceAssignment.property!!.value!!
259 fun transformToRARuntimeService(
260 blueprintRuntimeService: BluePrintRuntimeService<*>,
261 templateArtifactName: String
262 ): ResourceAssignmentRuntimeService {
264 val resourceAssignmentRuntimeService = ResourceAssignmentRuntimeService(
265 blueprintRuntimeService.id(),
266 blueprintRuntimeService.bluePrintContext()
268 resourceAssignmentRuntimeService.createUniqueId(templateArtifactName)
269 resourceAssignmentRuntimeService.setExecutionContext(blueprintRuntimeService.getExecutionContext() as MutableMap<String, JsonNode>)
271 return resourceAssignmentRuntimeService
274 @Throws(BluePrintProcessorException::class)
276 raRuntimeService: ResourceAssignmentRuntimeService,
277 dataTypeName: String,
280 lateinit var type: String
283 checkNotNull(raRuntimeService.bluePrintContext().dataTypeByName(dataTypeName)?.properties)
285 val propertyDefinition = checkNotNull(dataTypeProps[propertyName])
286 type = checkNotEmpty(propertyDefinition.type) { "Couldn't get data type ($dataTypeName)" }
287 logger.trace("Data type({})'s property ({}) is ({})", dataTypeName, propertyName, type)
288 } catch (e: Exception) {
289 logger.error("couldn't get data type($dataTypeName)'s property ($propertyName), error message $e")
290 throw BluePrintProcessorException("${e.message}", e)
295 @Throws(BluePrintProcessorException::class)
296 fun parseResponseNode(
297 responseNode: JsonNode,
298 resourceAssignment: ResourceAssignment,
299 raRuntimeService: ResourceAssignmentRuntimeService,
300 outputKeyMapping: MutableMap<String, String>
302 val metadata = resourceAssignment.property!!.metadata
304 if ((resourceAssignment.property?.type).isNullOrEmpty()) {
305 throw BluePrintProcessorException("Couldn't get data dictionary type for dictionary name (${resourceAssignment.name})")
307 val type = resourceAssignment.property!!.type
308 val valueToPrint = getValueToLog(metadata, responseNode)
310 logger.info("For template key (${resourceAssignment.name}) trying to get value from responseNode ($valueToPrint)")
312 in BluePrintTypes.validPrimitiveTypes() -> {
314 parseResponseNodeForPrimitiveTypes(responseNode, resourceAssignment, outputKeyMapping)
316 in BluePrintTypes.validCollectionTypes() -> {
318 parseResponseNodeForCollection(responseNode, resourceAssignment, raRuntimeService, outputKeyMapping)
322 parseResponseNodeForComplexType(responseNode, resourceAssignment, raRuntimeService, outputKeyMapping)
325 } catch (e: Exception) {
326 logger.error("Fail to parse response data, error message $e")
327 throw BluePrintProcessorException("${e.message}", e)
331 private fun parseResponseNodeForPrimitiveTypes(
332 responseNode: JsonNode,
333 resourceAssignment: ResourceAssignment,
334 outputKeyMapping: MutableMap<String, String>
336 // Return responseNode if is not a Complex Type
337 if (!responseNode.isComplexType()) {
341 val outputKey = outputKeyMapping.keys.firstOrNull()
342 var returnNode = if (responseNode is ArrayNode) {
343 val arrayNode = responseNode.toList()
344 if (outputKey.isNullOrEmpty()) {
347 arrayNode.firstOrNull { element ->
348 element.isComplexType() && element.has(outputKeyMapping[outputKey])
355 if (returnNode.isNullOrMissing() || returnNode!!.isComplexType() && !returnNode.has(outputKeyMapping[outputKey])) {
356 throw BluePrintProcessorException("Fail to find output key mapping ($outputKey) in the responseNode.")
359 val returnValue = if (returnNode.isComplexType()) {
360 returnNode[outputKeyMapping[outputKey]]
365 outputKey?.let { KeyIdentifier(it, returnValue) }
366 ?.let { resourceAssignment.keyIdentifiers.add(it) }
370 private fun parseResponseNodeForCollection(
371 responseNode: JsonNode,
372 resourceAssignment: ResourceAssignment,
373 raRuntimeService: ResourceAssignmentRuntimeService,
374 outputKeyMapping: MutableMap<String, String>
376 val dName = resourceAssignment.dictionaryName
377 val metadata = resourceAssignment.property!!.metadata
378 var resultNode: JsonNode
379 if ((resourceAssignment.property?.entrySchema?.type).isNullOrEmpty()) {
380 throw BluePrintProcessorException(
381 "Couldn't get data type for dictionary type " +
382 "(${resourceAssignment.property!!.type}) and dictionary name ($dName)"
385 val entrySchemaType = resourceAssignment.property!!.entrySchema!!.type
387 var arrayNode = JacksonUtils.objectMapper.createArrayNode()
388 if (outputKeyMapping.isNotEmpty()) {
389 when (responseNode) {
391 val responseArrayNode = responseNode.toList()
392 for (responseSingleJsonNode in responseArrayNode) {
393 val arrayChildNode = parseSingleElementOfArrayResponseNode(
394 entrySchemaType, resourceAssignment,
395 outputKeyMapping, raRuntimeService, responseSingleJsonNode, metadata
397 arrayNode.add(arrayChildNode)
399 resultNode = arrayNode
402 val responseArrayNode = responseNode.rootFieldsToMap()
404 parseObjectResponseNode(
405 resourceAssignment, entrySchemaType, outputKeyMapping,
406 responseArrayNode, metadata
410 throw BluePrintProcessorException("Key-value response expected to match the responseNode.")
414 when (responseNode) {
416 responseNode.forEach { elementNode ->
417 arrayNode.add(elementNode)
419 resultNode = arrayNode
422 val responseArrayNode = responseNode.rootFieldsToMap()
423 for ((key, responseSingleJsonNode) in responseArrayNode) {
424 val arrayChildNode = JacksonUtils.objectMapper.createObjectNode()
425 logKeyValueResolvedResource(metadata, key, responseSingleJsonNode, entrySchemaType)
426 JacksonUtils.populateJsonNodeValues(
428 responseSingleJsonNode,
432 arrayNode.add(arrayChildNode)
434 resultNode = arrayNode
437 resultNode = responseNode
445 private fun parseSingleElementOfArrayResponseNode(
446 entrySchemaType: String,
447 resourceAssignment: ResourceAssignment,
448 outputKeyMapping: MutableMap<String, String>,
449 raRuntimeService: ResourceAssignmentRuntimeService,
450 responseNode: JsonNode,
451 metadata: MutableMap<String, String>?
453 val outputKeyMappingHasOnlyOneElement = checkIfOutputKeyMappingProvideOneElement(outputKeyMapping)
454 when (entrySchemaType) {
455 in BluePrintTypes.validPrimitiveTypes() -> {
456 if (outputKeyMappingHasOnlyOneElement) {
457 val outputKeyMap = outputKeyMapping.entries.first()
458 if (resourceAssignment.keyIdentifiers.none { it.name == outputKeyMap.key }) {
459 resourceAssignment.keyIdentifiers.add(
460 KeyIdentifier(outputKeyMap.key, JacksonUtils.objectMapper.createArrayNode())
463 return parseSingleElementNodeWithOneOutputKeyMapping(
472 throw BluePrintProcessorException("Expect one entry in output-key-mapping")
477 checkOutputKeyMappingAllElementsInDataTypeProperties(
482 parseSingleElementNodeWithAllOutputKeyMapping(
490 outputKeyMappingHasOnlyOneElement -> {
491 val outputKeyMap = outputKeyMapping.entries.first()
492 parseSingleElementNodeWithOneOutputKeyMapping(
502 throw BluePrintProcessorException("Output-key-mapping do not map the Data Type $entrySchemaType")
509 private fun parseObjectResponseNode(
510 resourceAssignment: ResourceAssignment,
511 entrySchemaType: String,
512 outputKeyMapping: MutableMap<String, String>,
513 responseArrayNode: MutableMap<String, JsonNode>,
514 metadata: MutableMap<String, String>?
516 val outputKeyMappingHasOnlyOneElement = checkIfOutputKeyMappingProvideOneElement(outputKeyMapping)
517 if (outputKeyMappingHasOnlyOneElement) {
518 val outputKeyMap = outputKeyMapping.entries.first()
519 val returnValue = parseObjectResponseNodeWithOneOutputKeyMapping(
520 responseArrayNode, outputKeyMap.key, outputKeyMap.value,
521 entrySchemaType, metadata
523 resourceAssignment.keyIdentifiers.add(KeyIdentifier(outputKeyMap.key, returnValue))
526 throw BluePrintProcessorException("Output-key-mapping do not map the Data Type $entrySchemaType")
530 private fun parseSingleElementNodeWithOneOutputKeyMapping(
531 resourceAssignment: ResourceAssignment,
532 responseSingleJsonNode: JsonNode,
533 outputKeyMappingKey: String,
534 outputKeyMappingValue: String,
536 metadata: MutableMap<String, String>?
538 val arrayChildNode = JacksonUtils.objectMapper.createObjectNode()
540 val responseKeyValue = if (responseSingleJsonNode.has(outputKeyMappingValue)) {
541 responseSingleJsonNode.get(outputKeyMappingValue)
543 NullNode.getInstance()
546 logKeyValueResolvedResource(metadata, outputKeyMappingKey, responseKeyValue, type)
547 JacksonUtils.populateJsonNodeValues(outputKeyMappingKey, responseKeyValue, type, arrayChildNode)
548 resourceAssignment.keyIdentifiers.find { it.name == outputKeyMappingKey && it.value.isArray }
551 (it.value as ArrayNode).add(responseKeyValue)
553 resourceAssignment.keyIdentifiers.add(
554 KeyIdentifier(outputKeyMappingKey, responseKeyValue))
556 return arrayChildNode
559 private fun parseSingleElementNodeWithAllOutputKeyMapping(
560 resourceAssignment: ResourceAssignment,
561 responseSingleJsonNode: JsonNode,
562 outputKeyMapping: MutableMap<String, String>,
564 metadata: MutableMap<String, String>?
566 val arrayChildNode = JacksonUtils.objectMapper.createObjectNode()
567 outputKeyMapping.map {
568 val responseKeyValue = if (responseSingleJsonNode.has(it.value)) {
569 responseSingleJsonNode.get(it.value)
571 NullNode.getInstance()
574 logKeyValueResolvedResource(metadata, it.key, responseKeyValue, type)
575 JacksonUtils.populateJsonNodeValues(it.key, responseKeyValue, type, arrayChildNode)
576 resourceAssignment.keyIdentifiers.add(KeyIdentifier(it.key, responseKeyValue))
578 return arrayChildNode
581 private fun parseObjectResponseNodeWithOneOutputKeyMapping(
582 responseArrayNode: MutableMap<String, JsonNode>,
583 outputKeyMappingKey: String,
584 outputKeyMappingValue: String,
586 metadata: MutableMap<String, String>?
588 val objectNode = JacksonUtils.objectMapper.createObjectNode()
589 val responseSingleJsonNode = responseArrayNode.filterKeys { key ->
590 key == outputKeyMappingValue
591 }.entries.firstOrNull()
593 if (responseSingleJsonNode == null) {
594 logKeyValueResolvedResource(metadata, outputKeyMappingKey, NullNode.getInstance(), type)
595 JacksonUtils.populateJsonNodeValues(outputKeyMappingKey, NullNode.getInstance(), type, objectNode)
597 logKeyValueResolvedResource(metadata, outputKeyMappingKey, responseSingleJsonNode.value, type)
598 JacksonUtils.populateJsonNodeValues(outputKeyMappingKey, responseSingleJsonNode.value, type, objectNode)
603 private fun parseResponseNodeForComplexType(
604 responseNode: JsonNode,
605 resourceAssignment: ResourceAssignment,
606 raRuntimeService: ResourceAssignmentRuntimeService,
607 outputKeyMapping: MutableMap<String, String>
609 val entrySchemaType = resourceAssignment.property!!.type
610 val dictionaryName = resourceAssignment.dictionaryName!!
611 val metadata = resourceAssignment.property!!.metadata
612 val outputKeyMappingHasOnlyOneElement = checkIfOutputKeyMappingProvideOneElement(outputKeyMapping)
614 if (outputKeyMapping.isNotEmpty()) {
616 checkOutputKeyMappingAllElementsInDataTypeProperties(
621 parseSingleElementNodeWithAllOutputKeyMapping(
629 outputKeyMappingHasOnlyOneElement -> {
630 val outputKeyMap = outputKeyMapping.entries.first()
631 parseSingleElementNodeWithOneOutputKeyMapping(
632 resourceAssignment, responseNode, outputKeyMap.key,
633 outputKeyMap.value, entrySchemaType, metadata
637 throw BluePrintProcessorException("Output-key-mapping do not map the Data Type $entrySchemaType")
641 val childNode = JacksonUtils.objectMapper.createObjectNode()
642 JacksonUtils.populateJsonNodeValues(dictionaryName, responseNode, entrySchemaType, childNode)
647 private fun checkOutputKeyMappingAllElementsInDataTypeProperties(
648 dataTypeName: String,
649 outputKeyMapping: MutableMap<String, String>,
650 raRuntimeService: ResourceAssignmentRuntimeService
652 val dataTypeProps = raRuntimeService.bluePrintContext().dataTypeByName(dataTypeName)?.properties
653 val result = outputKeyMapping.filterKeys { !dataTypeProps!!.containsKey(it) }.keys.firstOrNull()
654 return result == null
657 private fun logKeyValueResolvedResource(
658 metadata: MutableMap<String, String>?,
663 val valueToPrint = getValueToLog(metadata, value)
666 "For List Type Resource: key ($key), value ($valueToPrint), " +
671 private fun checkIfOutputKeyMappingProvideOneElement(outputKeyMapping: MutableMap<String, String>): Boolean {
672 return (outputKeyMapping.size == 1)
675 fun getValueToLog(metadata: MutableMap<String, String>?, value: Any): Any =
676 if (hasLogProtect(metadata)) LOG_REDACTED else value