diff --git a/Changelog.md b/Changelog.md index a70973f58..707d97336 100644 --- a/Changelog.md +++ b/Changelog.md @@ -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). diff --git a/core/base/src/commonMain/kotlin/nl/adaptivity/xmlutil/util/CompactFragmentSerializer.kt b/core/base/src/commonMain/kotlin/nl/adaptivity/xmlutil/util/CompactFragmentSerializer.kt index e396bb246..c0269ba73 100644 --- a/core/base/src/commonMain/kotlin/nl/adaptivity/xmlutil/util/CompactFragmentSerializer.kt +++ b/core/base/src/commonMain/kotlin/nl/adaptivity/xmlutil/util/CompactFragmentSerializer.kt @@ -44,7 +44,6 @@ public object CompactFragmentSerializer : XmlSerializer { ): CompactFragment { return when { isValueChild -> { - input.next() input.siblingsToFragment() } diff --git a/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/XMLDecoder.kt b/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/XMLDecoder.kt index 048a6f659..406da7f8f 100644 --- a/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/XMLDecoder.kt +++ b/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/XMLDecoder.kt @@ -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) @@ -121,7 +120,6 @@ internal open class XmlDecoderBase internal constructor( ) : XmlCodec(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 @@ -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 @@ -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") @@ -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 decodeSerializableValue(deserializer: DeserializationStrategy): T { val effectiveDeserializationStrategy = xmlDescriptor.effectiveDeserializationStrategy(deserializer) return when (effectiveDeserializationStrategy) { is XmlDeserializationStrategy -> - effectiveDeserializationStrategy.deserializeXML(this, getInputWrapper(locationInfo)) + effectiveDeserializationStrategy.deserializeXML(this, input) else -> effectiveDeserializationStrategy.deserialize(this) } @@ -669,18 +665,24 @@ internal open class XmlDecoderBase internal constructor( internal inner class TagDecoder( deserializer: DeserializationStrategy<*>, xmlDescriptor: D, - typeDiscriminatorName: QName? + typeDiscriminatorName: QName?, ) : TagDecoderBase(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) } @@ -688,7 +690,7 @@ internal open class XmlDecoderBase internal constructor( @OptIn(ExperimentalXmlUtilApi::class) internal abstract inner class TagDecoderBase( - private val deserializer: DeserializationStrategy<*>, + protected val deserializer: DeserializationStrategy<*>, xmlDescriptor: D, protected val typeDiscriminatorName: QName? ) : XmlTagCodec(xmlDescriptor), CompositeDecoder, XML.XmlInput, TagIdHolder { @@ -816,6 +818,7 @@ internal open class XmlDecoderBase internal constructor( ): T { @Suppress("UNCHECKED_CAST") handleRecovery(index) { return it as T } + require(stage < STAGE_CLOSE) { "Reading content in end state" } val initialChildXmlDescriptor = xmlDescriptor.getElementDescriptor(index) @@ -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 @@ -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 @@ -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 } } @@ -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, @@ -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 @@ -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()) { @@ -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 } @@ -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 } } diff --git a/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/impl/PseudoBufferedReader.kt b/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/impl/PseudoBufferedReader.kt index 1d46b3977..92a0bf327 100644 --- a/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/impl/PseudoBufferedReader.kt +++ b/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/impl/PseudoBufferedReader.kt @@ -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() diff --git a/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/TestSoapHelper.kt b/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/TestSoapHelper.kt index ffe768f30..3cdd2da76 100644 --- a/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/TestSoapHelper.kt +++ b/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/TestSoapHelper.kt @@ -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.* @@ -45,7 +46,7 @@ class TestSoapHelper { @Test fun testRoundtripSoapResponse() { val xml = XML { indent = 2; autoPolymorphic = true } - val serializer = Envelope.Serializer(CompactFragmentSerializer) + val serializer = serializer>() val env: Envelope = xml.decodeFromString(serializer, SOAP_RESPONSE1) assertXmlEquals(SOAP_RESPONSE1_BODY, env.body.child.contentString.trim()) diff --git a/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/soap/Body.kt b/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/soap/Body.kt index b0c01645a..01f03bc83 100644 --- a/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/soap/Body.kt +++ b/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/soap/Body.kt @@ -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 @@ -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 @@ -65,11 +64,14 @@ import nl.adaptivity.xmlutil.util.CompactFragment * ``` * */ +@Serializable class Body( @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 = emptyMap(), + @XmlOtherAttributes + val otherAttributes: Map = emptyMap(), ) { fun copy( encodingStyle: String? = this.encodingStyle, @@ -82,22 +84,22 @@ class Body( otherAttributes: Map = this.otherAttributes, ): Body = Body(child, encodingStyle, otherAttributes) - class Serializer(private val contentSerializer: KSerializer): XmlSerializer> { + class Serializer(private val contentSerializer: KSerializer): KSerializer> { @OptIn(ExperimentalSerializationApi::class, XmlUtilInternal::class) override val descriptor: SerialDescriptor = buildClassSerialDescriptor("org.w3c.dom.Body") { annotations = SoapSerialObjects.bodyAnnotations element("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("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 { var encodingStyle: String? = null @@ -119,7 +121,7 @@ class Body( return Body(child) } - override fun deserializeXML( + /*override*/ fun deserializeXML( decoder: Decoder, input: XmlReader, previousValue: Body?, @@ -144,6 +146,7 @@ class Body( } }.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") } @@ -168,7 +171,7 @@ class Body( } } - override fun serializeXML(encoder: Encoder, output: XmlWriter, value: Body, isValueChild: Boolean) { + /*override*/ fun serializeXML(encoder: Encoder, output: XmlWriter, value: Body, isValueChild: Boolean) { output.smartStartTag(ELEMENTNAME) { value.encodingStyle?.also { style -> output.attribute(Envelope.NAMESPACE, "encodingStyle", Envelope.PREFIX, style.toString()) diff --git a/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/soap/Envelope.kt b/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/soap/Envelope.kt index 7d5db3a2e..6a0957ef7 100644 --- a/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/soap/Envelope.kt +++ b/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/soap/Envelope.kt @@ -92,7 +92,7 @@ class Envelope( constructor(content: T) : this(Body(content)) public class Serializer(bodyContentSerializer: KSerializer) : /*Xml*/KSerializer> { - private val bodySerializer: KSerializer> = Body.Serializer(bodyContentSerializer) + private val bodySerializer: KSerializer> = Body.serializer(bodyContentSerializer) @OptIn(XmlUtilInternal::class) override val descriptor: SerialDescriptor =