Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/TS-1127' into dev-version-2
Browse files Browse the repository at this point in the history
  • Loading branch information
OptimumCode committed Sep 28, 2023
2 parents cd0406c + 638e64f commit 193848e
Show file tree
Hide file tree
Showing 20 changed files with 902 additions and 110 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Codec Xml via Xsd
# Codec Xml via Xsd (2.2.0)
![version](https://img.shields.io/badge/version-0.0.4-blue.svg)

# How it works:
Expand Down Expand Up @@ -57,6 +57,8 @@ Error from validation process can be disabled for test purposes by `dirtyValidat
### Configuration example

* typePointer - Path to message type value for decode (null by default)

## Temporary disabled
* 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)

Expand All @@ -77,7 +79,7 @@ spec:
custom-config:
codecSettings:
typePointer: /root/node/node2/type
dirtyValidation: false
# dirtyValidation: false
```

## Required pins
Expand Down Expand Up @@ -142,6 +144,10 @@ spec:
## Changelog
### v2.2.0
* Migrate to StAX parser
### v2.1.0
* th2 transport protocol support
Expand Down
7 changes: 5 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,20 @@ dependencies {
api platform("com.exactpro.th2:bom:4.5.0")
implementation "com.exactpro.th2:common:5.4.0-dev"
implementation "com.exactpro.th2:codec:5.3.0-dev"
implementation "com.exactpro.th2:common-utils:2.2.0-dev"

implementation "org.slf4j:slf4j-api"
implementation "io.github.microutils:kotlin-logging:3.0.5"

implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlin:kotlin-reflect"

implementation "com.fasterxml.jackson.core:jackson-core"
implementation "com.github.javadev:underscore:1.93"
implementation "commons-io:commons-io"

implementation group: 'org.apache.ws.xmlschema', name: 'xmlschema-core', version: '2.3.0'

testImplementation "org.jetbrains.kotlin:kotlin-test-junit5:$kotlin_version"

compileOnly "com.google.auto.service:auto-service:1.1.1"
Expand Down
165 changes: 165 additions & 0 deletions src/main/kotlin/com/exactpro/th2/codec/xml/NodeContent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright 2022-2023 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

import com.exactpro.th2.common.grpc.Value
import com.exactpro.th2.common.value.toValue
import javax.xml.namespace.QName

typealias FieldName = String

interface FieldAppender<T> {
fun T.appendSimple(name: FieldName, value: String)

fun T.appendNode(name: FieldName, node: NodeContent<T>)

fun T.appendNodeCollection(name: FieldName, nodes: List<NodeContent<T>>)
}

class NodeContent<T>(
private val nodeName: QName,
decorator: XmlCodecStreamReader<T>,
messageSupplier: () -> T,
private val appender: FieldAppender<T>,
) {
private val messageBuilder: T by lazy(messageSupplier)
private val textSB = StringBuilder()

private val childNodes: MutableMap<QName, MutableList<NodeContent<T>>> = mutableMapOf()
var isMessage: Boolean = false
private set
private val isEmpty: Boolean
get() = !isMessage && textSB.isEmpty()

val name: String = nodeName.toNodeName()

init {
decorator.namespaceCount.let { size ->
if (size > 0) {
for (i in 0 until size) {
decorator.getNamespaceURI(i).also { value ->
val prefix = decorator.namespaceContext.getPrefix(value)

with(appender) {
messageBuilder.appendSimple(makeFieldName(NAMESPACE, prefix, true), value)
}
}
}
isMessage = true
}
}
decorator.attributeCount.let { size ->
if (size > 0) {
for (i in 0 until size) {
val localPart = decorator.getAttributeLocalName(i)
val prefix = decorator.getAttributePrefix(i)

with(appender) {
messageBuilder.appendSimple(makeFieldName(prefix, localPart, true), decorator.getAttributeValue(i))
}
}
isMessage = true
}
}
}

fun putChild(name: QName, node: NodeContent<T>) {
childNodes.compute(name) { _, value ->
value
?.apply { add(node) }
?: mutableListOf(node)
}
isMessage = true
}

fun appendText(text: String) {
if (text.isNotBlank()) {
textSB.append(text)
}
}

fun release() {
if (isMessage) {
childNodes.forEach { (name, values) ->
try {
val notEmptyValues = values.filterNot(NodeContent<*>::isEmpty)

if (notEmptyValues.isNotEmpty()) {
val first = notEmptyValues.first()
with(appender) {
when (values.size) { // Clarify type of element: list or single
0 -> error("Sub element $name hasn't got values")
1 -> messageBuilder.appendNode(first.name, first)
else -> messageBuilder.appendNodeCollection(
first.name,
notEmptyValues,
)
}
}
}
} catch (e: RuntimeException) {
throw IllegalStateException("The `$name` field can't be released in the `$nodeName` node", e)
}
}
if(textSB.isNotBlank()) {
with(appender) {
messageBuilder.appendSimple(TEXT_FIELD_NAME, textSB.toString())
}
}
}
}

fun toMessage(): T {
check(isMessage) {
"The $nodeName node isn't message"
}
return messageBuilder
}

fun toText(): String {
check(!isMessage) {
"The $nodeName is a message"
}
return textSB.toString()
}

override fun toString(): String {
return "NodeContent(nodeName=$nodeName, childNodes=$childNodes, text=$textSB)"
}

private fun Sequence<Value>.toListValue(): Value = Value.newBuilder().apply {
listValueBuilder.apply {
forEach(::addValues)
}
}.build()

private fun toValue(): Value = if (isMessage) {
messageBuilder.toValue()
} else {
textSB.toValue()
}

companion object {
private const val NAMESPACE = "xmlns"
private const val TEXT_FIELD_NAME = "#text"

private fun QName.toNodeName(): String = makeFieldName(prefix, localPart)

private fun makeFieldName(first: String, second: String, isAttribute: Boolean = false): String {
return "${if (isAttribute) "-" else ""}$first${if (first.isNotBlank() && second.isNotBlank()) ":" else ""}$second"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2023 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

object TransportFieldAppender : FieldAppender<MutableMap<String, Any>> {
override fun MutableMap<String, Any>.appendSimple(name: FieldName, value: String) {
put(name, value)
}

override fun MutableMap<String, Any>.appendNode(name: FieldName, node: NodeContent<MutableMap<String, Any>>) {
put(name, node.extractValue())
}

override fun MutableMap<String, Any>.appendNodeCollection(
name: FieldName,
nodes: List<NodeContent<MutableMap<String, Any>>>,
) {
put(name, nodes.asSequence().map { it.extractValue() }.toList())
}

private fun NodeContent<MutableMap<String, Any>>.extractValue(): Any =
if (isMessage) {
toMessage()
} else {
toText()
}
}
80 changes: 80 additions & 0 deletions src/main/kotlin/com/exactpro/th2/codec/xml/XmlCodecStreamReader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2022-2023 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

import java.io.ByteArrayInputStream
import java.util.ArrayDeque
import javax.xml.stream.XMLInputFactory
import javax.xml.stream.XMLStreamException
import javax.xml.stream.util.StreamReaderDelegate

class XmlCodecStreamReader<T>(
body: ByteArray,
private val messageSupplier: () -> T,
private val appender: FieldAppender<T>,
)
: StreamReaderDelegate(
XML_INPUT_FACTORY.createXMLStreamReader(
ByteArrayInputStream(body)
)
), AutoCloseable {
private lateinit var rootNode: NodeContent<T>
private lateinit var messageType: String

private val elements = ArrayDeque<NodeContent<T>>()

@Throws(XMLStreamException::class)
override fun next(): Int = super.next().also { eventCode ->
when (eventCode) {
START_ELEMENT -> {
val qName = name
NodeContent(qName, this, messageSupplier, appender).also { nodeContent ->
if (elements.isNotEmpty()) {
elements.peek()
.putChild(qName, nodeContent)
}

elements.push(nodeContent)

if (!this::messageType.isInitialized) {
messageType = localName
}
if (!this::rootNode.isInitialized) {
rootNode = nodeContent
}
}
}
CHARACTERS -> elements.peek().appendText(text)
END_ELEMENT -> elements.pop().also(NodeContent<*>::release)
}
}

fun getMessage(): T {
check(elements.isEmpty()) {
"Some of XML nodes aren't closed ${elements.joinToString { it.name }}"
}

return messageSupplier().apply {
with(appender) {
appendNode(rootNode.name, rootNode)
}
}
}

companion object {
private val XML_INPUT_FACTORY = XMLInputFactory.newInstance()
}
}
Loading

0 comments on commit 193848e

Please sign in to comment.