Skip to content

Commit

Permalink
[TH2-3162] Xml headers (#8) (#9)
Browse files Browse the repository at this point in the history
Xml declaration processing
  • Loading branch information
Topru333 authored Mar 24, 2022
1 parent 75a7af7 commit 869a4da
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 156 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Codec Xml via Xsd
![version](https://img.shields.io/badge/version-0.0.2-blue.svg)
![version](https://img.shields.io/badge/version-0.0.3-blue.svg)

# How it works:

Expand Down Expand Up @@ -58,10 +58,12 @@ Error from validation process can be disabled for test purposes by `dirtyValidat

* typePointer - Path to message type value for decode (null by default)
* dirtyValidation - Disable/enable error during validation phase. If disabled all errors will be only visible in log (false by default)
* expectsDeclaration - Disable/enable validation of declaration - is it exist or not (true by default)

```yaml
typePointer: /root/node/node2/type
dirtyValidation: false
expectsDeclaration: false
```
For example:
Expand Down Expand Up @@ -140,6 +142,13 @@ spec:

## Changelog

### v0.0.3

#### Feature:

* Declaration check option
* Json step as converter was removed [ Message -> **Json** -> Map -> XmlString -> Xml ] to [ Message -> Map -> XmlString -> Xml ]

### v0.0.2

#### Feature:
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.
#

release_version=0.0.2
release_version=0.0.3
kotlin_version=1.5.31
description = 'th2 codec xml via xsd'

Expand Down
77 changes: 52 additions & 25 deletions src/main/kotlin/com/exactpro/th2/codec/xml/XmlPipelineCodec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,26 @@ package com.exactpro.th2.codec.xml

import com.exactpro.th2.codec.DecodeException
import com.exactpro.th2.codec.api.IPipelineCodec
import com.exactpro.th2.codec.xml.utils.toMap
import com.exactpro.th2.codec.xml.utils.toProto
import com.exactpro.th2.codec.xml.xsd.XsdValidator
import com.exactpro.th2.common.grpc.AnyMessage
import com.exactpro.th2.common.grpc.Message
import com.exactpro.th2.common.grpc.MessageGroup
import com.exactpro.th2.common.grpc.RawMessage
import com.exactpro.th2.common.message.toJson
import com.exactpro.th2.codec.xml.utils.toJson
import com.exactpro.th2.codec.xml.utils.toProto
import com.exactpro.th2.codec.xml.xsd.XsdValidator
import com.exactpro.th2.common.message.logId
import com.exactpro.th2.common.message.messageType
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.json.JsonMapper
import com.github.underscore.lodash.Json
import com.github.underscore.lodash.U
import com.exactpro.th2.common.message.toJson
import com.github.underscore.lodash.Xml
import com.google.protobuf.ByteString
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.nio.charset.Charset
import java.nio.file.Path


open class XmlPipelineCodec(private val settings: XmlPipelineCodecSettings, xsdMap: Map<String, Path>) : IPipelineCodec {

private val pointer = settings.typePointer?.split("/")?.filterNot { it.isBlank() }
private var xmlCharset: Charset = Charsets.UTF_8
private val validator = XsdValidator(xsdMap, settings.dirtyValidation)

Expand All @@ -64,15 +62,14 @@ open class XmlPipelineCodec(private val settings: XmlPipelineCodecSettings, xsdM

private fun encodeOne(message: Message): RawMessage {

val json = message.toJson()

val xmlString = U.jsonToXml(json)
val map = message.toMap()
val xmlString = Xml.toXml(map)

validator.validate(xmlString.toByteArray())
LOGGER.info("Validation of incoming parsed message complete: ${message.messageType}")
LOGGER.debug("Validation of incoming parsed message complete: ${message.messageType}")

return RawMessage.newBuilder().apply {
parentEventId = message.parentEventId
if (message.hasParentEventId()) parentEventId = message.parentEventId
metadataBuilder.putAllProperties(message.metadata.propertiesMap)
metadataBuilder.protocol = XmlPipelineCodecFactory.PROTOCOL
metadataBuilder.id = message.metadata.id
Expand Down Expand Up @@ -106,29 +103,59 @@ open class XmlPipelineCodec(private val settings: XmlPipelineCodecSettings, xsdM
private fun decodeOne(rawMessage: RawMessage): Message {
try {
validator.validate(rawMessage.body.toByteArray())
LOGGER.info("Validation of incoming raw message complete: ${rawMessage.metadata.idOrBuilder}")
LOGGER.debug("Validation of incoming raw message complete: ${rawMessage.logId}")
val xmlString = rawMessage.body.toStringUtf8()
val jsonString = U.xmlToJson(xmlString, Json.JsonStringBuilder.Step.COMPACT, null )
@Suppress("UNCHECKED_CAST")
val map = Xml.fromXml(xmlString) as MutableMap<String, *>

val jsonNode: JsonNode = jsonMapper.readTree(jsonString)
LOGGER.trace("Result of the 'Xml.fromXml' method is ${map.keys} for $xmlString")
map -= STANDALONE
map -= ENCODING

check(jsonNode.size()==1) {"There was more than one root node in processed xml, result json have ${jsonNode.size()}"}
if (OMIT_XML_DECLARATION in map) {
// U library will tell by this option is there no declaration
check(!settings.expectsDeclaration || map[OMIT_XML_DECLARATION] == NO) { "Expecting declaration inside xml data" }
map -= OMIT_XML_DECLARATION
}

val msgType: String = settings.typePointer?.let {
val typeNode = jsonNode.at(it)
typeNode.asText()
} ?: jsonNode.fieldNames().next()
if (map.size > 1) {
error("There was more than one root node in processed xml, result json has [${map.size}]: ${map.keys.joinToString(", ")}")
}

check(jsonNode.size()==1) {"There more then one root messages after xml to Node process"}
val msgType: String = pointer?.let { map.getNode<String>(it) } ?: map.keys.first()

return jsonNode.toProto(msgType, rawMessage)
return map.toProto(msgType, rawMessage)
} catch (e: Exception) {
throw DecodeException("Can not decode message. Can not parse XML. ${rawMessage.body.toStringUtf8()}", e)
}
}

private inline fun <reified T>Map<*,*>.getNode(pointer: List<String>): T {
var current: Any = this

for (name in pointer) {
current = (current as? Map<*, *>)?.get(name) ?: error("Can not find element by name '$name' in path: $pointer")
}
return current as T
}


companion object {
private val LOGGER: Logger = LoggerFactory.getLogger(XmlPipelineCodec::class.java)
private val jsonMapper = JsonMapper()

private const val NO = "no"

/**
* The constant from [Xml.OMITXMLDECLARATION]
*/
private const val OMIT_XML_DECLARATION = "#omit-xml-declaration"
/**
* The constant from [Xml.ENCODING]
*/
private const val ENCODING = "#encoding"
/**
* The constant from [Xml.STANDALONE]
*/
private const val STANDALONE = "#standalone"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ package com.exactpro.th2.codec.xml

import com.exactpro.th2.codec.api.IPipelineCodecSettings

class XmlPipelineCodecSettings(val typePointer: String? = null, val dirtyValidation: Boolean = false) : IPipelineCodecSettings {
}
class XmlPipelineCodecSettings(
val typePointer: String? = null,
val dirtyValidation: Boolean = false,
val expectsDeclaration: Boolean = true,
) : IPipelineCodecSettings
70 changes: 32 additions & 38 deletions src/main/kotlin/com/exactpro/th2/codec/xml/utils/Converter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,54 +16,48 @@
package com.exactpro.th2.codec.xml.utils

import com.exactpro.th2.codec.xml.XmlPipelineCodecFactory
import com.exactpro.th2.common.grpc.ListValue
import com.exactpro.th2.common.grpc.Message
import com.exactpro.th2.common.grpc.RawMessage
import com.exactpro.th2.common.grpc.Value
import com.exactpro.th2.common.message.*
import com.exactpro.th2.common.message.message
import com.exactpro.th2.common.message.messageType
import com.exactpro.th2.common.message.set
import com.exactpro.th2.common.value.getMessage
import com.exactpro.th2.common.value.toValue
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.node.ValueNode
import java.lang.IllegalArgumentException

private fun JsonNode.toProtoValue(name: String = ""): Value = when (this) {
is ObjectNode -> message().also { builder ->
@Suppress("UNCHECKED_CAST")
private fun Map<String, *>.toProtoValue(name: String = ""): Value {
val message = message().also { builder ->
builder.messageType = name
fieldNames().forEach {
builder.putFields(it, this[it].toProtoValue())
}
}.build().toValue()
is ArrayNode -> Value.newBuilder().also { builder ->
val listValueBuilder = ListValue.newBuilder()
forEach {
listValueBuilder.addValues(it.toProtoValue())
}
for ((key, value) in this) {
message[key] = when (value) {
is Map<*, *> -> (value as Map<String, *>).toProtoValue()
is String -> value
is ArrayList<*> -> when {
value[0] is Map<*, *> -> value.map { element -> (element as Map<String, *>).toProtoValue() }.toValue()
else -> value.toValue()
}
null -> continue
else -> error("Unsupported type of value: ${value::class.simpleName}")
}
builder.listValue = listValueBuilder.build()
}.build()
is ValueNode -> Value.newBuilder().apply {
simpleValue = textValue()
}.build()
else -> error("Unknown node type ${this::class.java}")
}
return message.build().toValue()
}

fun JsonNode.toProto(type: String, rawMessage: RawMessage): Message = this.toProtoValue(type).getMessage()!!.toBuilder().also { builder ->
builder.parentEventId = rawMessage.parentEventId
builder.metadataBuilder.also { msgMetadata ->
rawMessage.metadata.also { rawMetadata ->
msgMetadata.id = rawMetadata.id
msgMetadata.timestamp = rawMetadata.timestamp
msgMetadata.protocol = XmlPipelineCodecFactory.PROTOCOL
msgMetadata.putAllProperties(rawMetadata.propertiesMap)
}
}
}.build() ?: throw IllegalArgumentException("JsonNode $this does not contain a message")
fun Map<String, *>.toProto(type: String, rawMessage: RawMessage): Message {
val builder = toProtoValue(type).getMessage()?.toBuilder() ?: throw IllegalArgumentException("JsonNode $this does not contain a message")
val rawMetadata = rawMessage.metadata

if (rawMessage.hasParentEventId()) builder.parentEventId = rawMessage.parentEventId

fun Message.toJson(): String = ObjectMapper().createObjectNode().apply {
fieldsMap.forEach {
putField(it.key, it.value)
builder.metadataBuilder.apply {
id = rawMetadata.id
timestamp = rawMetadata.timestamp
protocol = XmlPipelineCodecFactory.PROTOCOL
putAllProperties(rawMetadata.propertiesMap)
}
}.toString()

return builder.build()
}
62 changes: 0 additions & 62 deletions src/main/kotlin/com/exactpro/th2/codec/xml/utils/JsonUtils.kt

This file was deleted.

38 changes: 38 additions & 0 deletions src/main/kotlin/com/exactpro/th2/codec/xml/utils/MessageUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2021-2022 Exactpro (Exactpro Systems Limited)
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.exactpro.th2.codec.xml.utils

import com.exactpro.th2.common.grpc.ListValue
import com.exactpro.th2.common.grpc.Message
import com.exactpro.th2.common.grpc.Value
import com.exactpro.th2.common.grpc.Value.KindCase.NULL_VALUE
import com.exactpro.th2.common.grpc.Value.KindCase.SIMPLE_VALUE
import com.exactpro.th2.common.grpc.Value.KindCase.MESSAGE_VALUE
import com.exactpro.th2.common.grpc.Value.KindCase.LIST_VALUE


fun Message.toMap(): MutableMap<String, Any?> = fieldsMap.mapValuesTo(LinkedHashMap()) { it.value.toObject() }

fun ListValue.toList(): MutableList<Any?> = MutableList(valuesCount) { valuesList[it].toObject() }

fun Value.toObject(): Any? = when (kindCase) {
NULL_VALUE -> null
SIMPLE_VALUE -> simpleValue
MESSAGE_VALUE -> messageValue.toMap()
LIST_VALUE -> listValue.toList()
else -> error("Unknown value kind: $this")
}

Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class XsdValidator(private val xsdMap: Map<String, Path>, private val dirtyValid
}
} catch (e: Exception) {
if (dirtyValidation) {
LOGGER.error("VALIDATION ERROR: ", e)
LOGGER.warn("VALIDATION ERROR: ", e)
} else {
throw e
}
Expand Down
Loading

0 comments on commit 869a4da

Please sign in to comment.