Skip to content

Commit

Permalink
Make @XmlValue work with regular types (to support generic parsing
Browse files Browse the repository at this point in the history
  of element content of variable type, some including mixed/text content)
  • Loading branch information
pdvrieze committed Sep 11, 2024
1 parent 21fe06e commit e485fb0
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 60 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Changes:
This aids #225.

Fixes:
- Make `@XmlValue` work with regular types (to support generic parsing
of element content of variable type, some including mixed/text content)
- Better support `XmlSerialName` where value (localname) is defaulted.
Change this behaviour to actually use the same algorithm as normally
(not using the FQCN).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ public object CompactFragmentSerializer : XmlSerializer<CompactFragment> {
): CompactFragment {
return when {
isValueChild -> {
input.next()
input.siblingsToFragment()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import nl.adaptivity.xmlutil.serialization.structure.*
import nl.adaptivity.xmlutil.util.CompactFragment
import nl.adaptivity.xmlutil.util.CompactFragmentSerializer
import nl.adaptivity.xmlutil.util.XmlBooleanSerializer
import kotlin.reflect.KClass
import nl.adaptivity.xmlutil.serialization.CompactFragmentSerializer as DeprecatedCompactFragmentSerializer

@OptIn(ExperimentalSerializationApi::class)
Expand Down Expand Up @@ -121,7 +120,6 @@ internal open class XmlDecoderBase internal constructor(
) : XmlCodec<XmlDescriptor>(xmlDescriptor), XML.XmlInput, Decoder {
final override val config: XmlConfig get() = this@XmlDecoderBase.config
final override val serializersModule: SerializersModule get() = this@XmlDecoderBase.serializersModule
final override val input: XmlPeekingReader get() = this@XmlDecoderBase.input

override fun decodeNull(): Nothing? {
// We don't write nulls, so if we know that we have a null we just return it
Expand Down Expand Up @@ -184,6 +182,7 @@ internal open class XmlDecoderBase internal constructor(
protected val isValueChild: Boolean = false,
val attrIndex: Int = -1,
) : DecodeCommons(xmlDescriptor), Decoder, XML.XmlInput, ChunkedDecoder {
final override val input: XmlPeekingReader get() = this@XmlDecoderBase.input

private var triggerInline = false

Expand Down Expand Up @@ -326,6 +325,7 @@ internal open class XmlDecoderBase internal constructor(
private val locationInfo: XmlReader.LocationInfo?,
private val stringValue: String
) : Decoder, XML.XmlInput, DecodeCommons(xmlDescriptor) {
override val input: XmlPeekingReader by lazy { PseudoBufferedReader(XmlStringReader(locationInfo, stringValue)) }

override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
throw UnsupportedOperationException("Strings cannot be decoded to structures")
Expand All @@ -345,15 +345,11 @@ internal open class XmlDecoderBase internal constructor(
return stringValue
}

private fun getInputWrapper(locationInfo: XmlReader.LocationInfo?): XmlReader {
return XmlStringReader(locationInfo, stringValue)
}

override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T {
val effectiveDeserializationStrategy = xmlDescriptor.effectiveDeserializationStrategy(deserializer)
return when (effectiveDeserializationStrategy) {
is XmlDeserializationStrategy ->
effectiveDeserializationStrategy.deserializeXML(this, getInputWrapper(locationInfo))
effectiveDeserializationStrategy.deserializeXML(this, input)

else -> effectiveDeserializationStrategy.deserialize(this)
}
Expand Down Expand Up @@ -669,26 +665,32 @@ internal open class XmlDecoderBase internal constructor(
internal inner class TagDecoder<D : XmlDescriptor>(
deserializer: DeserializationStrategy<*>,
xmlDescriptor: D,
typeDiscriminatorName: QName?
typeDiscriminatorName: QName?,
) : TagDecoderBase<D>(deserializer, xmlDescriptor, typeDiscriminatorName) {

private val readTagName = if(config.isUnchecked) xmlDescriptor.tagName else input.name

override fun endStructure(descriptor: SerialDescriptor) {
// If we aren't in the closed stage, we read the index again to check that we are finished
if (stage < STAGE_CLOSE) {
// Don't do the check for custom strategies
if (stage < STAGE_CLOSE && deserializer !is XmlDeserializationStrategy) {
stage = STAGE_NULLS
val index = decodeElementIndex()
if (index != CompositeDecoder.DECODE_DONE) throw XmlSerialException("Unexpected content in end structure")
if (true && index != CompositeDecoder.DECODE_DONE) throw XmlSerialException(
"Unexpected content in end structure: ${
xmlDescriptor.friendlyChildName(index)
}"
)
}
check(input.depth == tagDepth) { "Unexpected tag depth: ${input.depth} (expected: ${tagDepth})" }
if (!config.isUnchecked) input.require(EventType.END_ELEMENT, readTagName)
}

}

@OptIn(ExperimentalXmlUtilApi::class)
internal abstract inner class TagDecoderBase<D : XmlDescriptor>(
private val deserializer: DeserializationStrategy<*>,
protected val deserializer: DeserializationStrategy<*>,
xmlDescriptor: D,
protected val typeDiscriminatorName: QName?
) : XmlTagCodec<D>(xmlDescriptor), CompositeDecoder, XML.XmlInput, TagIdHolder {
Expand Down Expand Up @@ -816,6 +818,7 @@ internal open class XmlDecoderBase internal constructor(
): T {
@Suppress("UNCHECKED_CAST")
handleRecovery<Any?>(index) { return it as T }
require(stage < STAGE_CLOSE) { "Reading content in end state" }

val initialChildXmlDescriptor = xmlDescriptor.getElementDescriptor(index)

Expand All @@ -829,38 +832,41 @@ internal open class XmlDecoderBase internal constructor(

val isValueChild = xmlDescriptor.getValueChild() == index

val decoder: Decoder = if (lastAttrIndex >= 0 && childXmlDescriptor is XmlAttributeMapDescriptor) {
AttributeMapDecoder(effectiveDeserializer, childXmlDescriptor, lastAttrIndex)
} else {
serialElementDecoder(descriptor, index, effectiveDeserializer)
?: NullDecoder(childXmlDescriptor, isValueChild)
}

val result: T = when (effectiveDeserializer) {
is XmlDeserializationStrategy -> {
check(!input.hasPeekItems)
check(input.eventType == EventType.START_ELEMENT)
val expectedDepth = when {
isValueChild -> input.depth + 1
else -> input.depth
}
val decoder = when {
stage == STAGE_NULLS -> NullDecoder(childXmlDescriptor, isValueChild)

val r = effectiveDeserializer.deserializeXML(decoder, input, previousValue, isValueChild)
lastAttrIndex >= 0 && childXmlDescriptor is XmlAttributeMapDescriptor -> {
AttributeMapDecoder(effectiveDeserializer, childXmlDescriptor, lastAttrIndex)
}

// Make sure that the end tag is not consumed - it will be consumed by the endStructure function
if (!input.hasPeekItems && input.eventType == EventType.END_ELEMENT) {
if (input.depth < expectedDepth) {
input.pushBackCurrent()
}
}
// handle empty value children separately
isValueChild && input.hasPeekItems && input.peekNextEvent() == EventType.END_ELEMENT ->
StringDecoder(childXmlDescriptor, input.extLocationInfo, "")

r
else -> {
serialElementDecoder(descriptor, index, effectiveDeserializer)
?: NullDecoder(childXmlDescriptor, isValueChild)
}
}

val effectiveInput: XmlPeekingReader = when (decoder) {
is NullDecoder -> decoder.input
is StringDecoder -> decoder.input
else -> input
}

val result: T = when (effectiveDeserializer) {
is XmlDeserializationStrategy ->
effectiveDeserializer.deserializeXML(decoder, effectiveInput, previousValue, isValueChild)

is AbstractCollectionSerializer<*, T, *> ->
effectiveDeserializer.merge(decoder, previousValue)

else -> try {
// For value children ignore whitespace content
if (isValueChild && !input.hasPeekItems && input.eventType == EventType.IGNORABLE_WHITESPACE) {
input.next()
}
effectiveDeserializer.deserialize(decoder)
} catch (e: XmlException) {
throw e
Expand All @@ -874,6 +880,18 @@ internal open class XmlDecoderBase internal constructor(
}
}

if (stage == STAGE_CONTENT) {
if (!input.hasPeekItems) {
if (isValueChild && input.eventType == EventType.END_ELEMENT && input.depth == tagDepth) {
input.pushBackCurrent() // unread the end of the containing element tag
}
} else { //has peek items
if (input.peekNextEvent() == EventType.END_ELEMENT && input.depth > tagDepth + 1) {
input.next() // consume peeked event if needed
}
}
}

val tagId = (decoder as? SerialValueDecoder)?.tagIdHolder?.tagId
if (tagId != null) {
checkNotNull(result) // only a non-null value can have an id
Expand Down Expand Up @@ -1091,17 +1109,17 @@ internal open class XmlDecoderBase internal constructor(
return pendingRecovery.first().elementIndex
}
if (stage == STAGE_NULLS) {
if (nulledItemsIdx >= 0) {
nextNulledItemsIdx()
if (stage == STAGE_NULLS) { // still reading nulls
// This processes all "missing" elements.
if (!config.isUnchecked) input.require(EventType.END_ELEMENT, xmlDescriptor.tagName)
if (!config.isUnchecked && !input.hasPeekItems) input.require(EventType.END_ELEMENT, xmlDescriptor.tagName)

if (nulledItemsIdx >= seenItems.size) return CompositeDecoder.DECODE_DONE

return nulledItemsIdx.also {// return the current index, and then move to the next value
nextNulledItemsIdx()
if (nulledItemsIdx >= seenItems.size) {
stage = STAGE_CLOSE
return CompositeDecoder.DECODE_DONE
}
} else {
stage = STAGE_CLOSE

return nulledItemsIdx
}
}

Expand Down Expand Up @@ -1168,12 +1186,21 @@ internal open class XmlDecoderBase internal constructor(
if ((!valueChildDesc.isNullable) && valueChildDesc.kind !is StructureKind.LIST && valueChildDesc.kind !is StructureKind.MAP) {
// This code can rely on seenItems to avoid infinite item loops as it only triggers on an empty tag.
seenItems[valueChild] = true

if(input.next() == EventType.END_ELEMENT) {
// empty value
input.pushBackCurrent()
}

return valueChild
}
}
for (eventType in input) {
when (eventType) {
EventType.END_ELEMENT -> return readElementEnd()
EventType.END_ELEMENT -> {
stage = STAGE_NULLS
return decodeElementIndex()
}

EventType.START_DOCUMENT,
EventType.COMMENT,
Expand Down Expand Up @@ -1346,7 +1373,7 @@ internal open class XmlDecoderBase internal constructor(
tagId = xmlCollapseWhitespace(a)
}
return a
} else if (nulledItemsIdx >= 0) { // Now reading nulls
} else if (stage == STAGE_NULLS) { // Now reading nulls
val default = (childDesc as? XmlValueDescriptor)?.default
return when {
default != null -> default
Expand Down Expand Up @@ -1838,7 +1865,7 @@ internal open class XmlDecoderBase internal constructor(
override fun Int.checkRepeat(): Int = this

override fun decodeElementIndex(): Int {

if (stage > STAGE_CONTENT) return CompositeDecoder.DECODE_DONE
if (!xmlDescriptor.isValueCollapsed) {
if (lastIndex.mod(2) == 1) {
while (input.hasNext()) {
Expand All @@ -1862,6 +1889,7 @@ internal open class XmlDecoderBase internal constructor(

EventType.END_ELEMENT -> {
check(super.decodeElementIndex() == CompositeDecoder.DECODE_DONE) { "Finished parsing map" }
stage = STAGE_CLOSE // no nulls
return CompositeDecoder.DECODE_DONE // should be the value
}

Expand All @@ -1879,6 +1907,7 @@ internal open class XmlDecoderBase internal constructor(

// Use the default, but correct the index (map serializer is dumb)
if (lastIndex.mod(2) == 1 && super.decodeElementIndex() < 0) {
stage = STAGE_CLOSE
return CompositeDecoder.DECODE_DONE // should be the value
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ internal class PseudoBufferedReader(private val delegate: XmlReader) : XmlPeekin
"Use extLocationInfo as that allows more detailed information",
replaceWith = ReplaceWith("extLocationInfo?.toString()")
)
override val locationInfo: String? get() = ifNotPeeking { locationInfo }
override val locationInfo: String? get() = delegate.locationInfo

override val extLocationInfo: XmlReader.LocationInfo?
get() = delegate.extLocationInfo

override fun hasNext(): Boolean = hasPeekItems || delegate.hasNext()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package nl.adaptivity.xml.serialization.regressions

import io.github.pdvrieze.xmlutil.testutil.assertXmlEquals
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.serializer
import nl.adaptivity.xml.serialization.regressions.soap.Envelope
import nl.adaptivity.xml.serialization.regressions.soap.Fault
import nl.adaptivity.xmlutil.*
Expand Down Expand Up @@ -45,7 +46,7 @@ class TestSoapHelper {
@Test
fun testRoundtripSoapResponse() {
val xml = XML { indent = 2; autoPolymorphic = true }
val serializer = Envelope.Serializer(CompactFragmentSerializer)
val serializer = serializer<Envelope<CompactFragment>>()
val env: Envelope<CompactFragment> = xml.decodeFromString(serializer, SOAP_RESPONSE1)
assertXmlEquals(SOAP_RESPONSE1_BODY, env.body.child.contentString.trim())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ package nl.adaptivity.xml.serialization.regressions.soap

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
Expand All @@ -36,9 +37,7 @@ import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure
import nl.adaptivity.serialutil.decodeElements
import nl.adaptivity.xmlutil.*
import nl.adaptivity.xmlutil.serialization.XML
import nl.adaptivity.xmlutil.serialization.XmlElement
import nl.adaptivity.xmlutil.serialization.XmlValue
import nl.adaptivity.xmlutil.serialization.*
import nl.adaptivity.xmlutil.util.CompactFragment


Expand All @@ -65,11 +64,14 @@ import nl.adaptivity.xmlutil.util.CompactFragment
* ```
*
*/
@Serializable
class Body<out T: Any>(
@XmlValue(true)
val child: T,
@XmlSerialName("encodingStyle", Envelope.NAMESPACE, Envelope.PREFIX)
val encodingStyle: String? = "http://www.w3.org/2003/05/soap-encoding",
val otherAttributes: Map<QName, String> = emptyMap(),
@XmlOtherAttributes
val otherAttributes: Map<SerializableQName, String> = emptyMap(),
) {
fun copy(
encodingStyle: String? = this.encodingStyle,
Expand All @@ -82,22 +84,22 @@ class Body<out T: Any>(
otherAttributes: Map<QName, String> = this.otherAttributes,
): Body<U> = Body(child, encodingStyle, otherAttributes)

class Serializer<T: Any>(private val contentSerializer: KSerializer<T>): XmlSerializer<Body<T>> {
class Serializer<T: Any>(private val contentSerializer: KSerializer<T>): KSerializer<Body<T>> {

@OptIn(ExperimentalSerializationApi::class, XmlUtilInternal::class)
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("org.w3c.dom.Body") {
annotations = SoapSerialObjects.bodyAnnotations
element<String>("encodingStyle", SoapSerialObjects.encodingStyleAnnotations, true)
element("otherAttributes", SoapSerialObjects.attrsSerializer.descriptor, isOptional = true)
element("otherAttributes", SoapSerialObjects.attrsSerializer.descriptor, listOf(XmlElement(false), XmlOtherAttributes()), isOptional = true)
element("child", contentSerializer.descriptor, SoapSerialObjects.valueAnnotations)
}.xml(
}/*.xml(
buildClassSerialDescriptor("org.w3c.dom.Body") {
annotations = SoapSerialObjects.bodyAnnotations
element<String>("encodingStyle", SoapSerialObjects.encodingStyleAnnotations, true)
element("otherAttributes", SoapSerialObjects.attrsSerializer.descriptor, listOf(XmlElement(false)), isOptional = true)
element("otherAttributes", SoapSerialObjects.attrsSerializer.descriptor, listOf(XmlElement(false), XmlOtherAttributes()), isOptional = true)
element("child", contentSerializer.descriptor, SoapSerialObjects.valueAnnotations)
}
)
)*/

override fun deserialize(decoder: Decoder): Body<T> {
var encodingStyle: String? = null
Expand All @@ -119,7 +121,7 @@ class Body<out T: Any>(
return Body(child)
}

override fun deserializeXML(
/*override*/ fun deserializeXML(
decoder: Decoder,
input: XmlReader,
previousValue: Body<T>?,
Expand All @@ -144,6 +146,7 @@ class Body<out T: Any>(
}
}.associate { QName(it.namespaceUri, it.localName, it.prefix) to it.value }
if (input.nextTag() != EventType.END_ELEMENT) {
// child = (decoder as XML.XmlInput).delegateFormat().decodeFromReader(contentSerializer, input)
child = decodeSerializableElement(descriptor, 2, contentSerializer, null)
if (input.nextTag() != EventType.END_ELEMENT) throw SerializationException("Extra content in body")
}
Expand All @@ -168,7 +171,7 @@ class Body<out T: Any>(
}
}

override fun serializeXML(encoder: Encoder, output: XmlWriter, value: Body<T>, isValueChild: Boolean) {
/*override*/ fun serializeXML(encoder: Encoder, output: XmlWriter, value: Body<T>, isValueChild: Boolean) {
output.smartStartTag(ELEMENTNAME) {
value.encodingStyle?.also { style ->
output.attribute(Envelope.NAMESPACE, "encodingStyle", Envelope.PREFIX, style.toString())
Expand Down
Loading

0 comments on commit e485fb0

Please sign in to comment.