Skip to content

Latest commit

 

History

History
443 lines (387 loc) · 18.1 KB

File metadata and controls

443 lines (387 loc) · 18.1 KB

Example Dynamic tag names

This example is based upon bug #41. It shows how to use a custom serializer with filtering to handle an "almostXml" format.

Output

<Container>
  <Test_123 attr="42">
    <data>someData</data>
  </Test_123>
  <Test_456 attr="71">
    <data>moreData</data>
  </Test_456>
</Container>

Data types

Container & TestElement - data.kt

/** Version that works with the released version 0.80.0 and 0.80.1 */
@Serializable(with = ContainerSerializer::class)
data class Container(val data: List<TestElement>)

@Serializable
data class TestElement(val id: Int, val attr: Int, @XmlElement(true) val data: String)

There are 2 example serializers. One works in 0.80.0 and 0.80.1 and the other one will work on the master/dev branches only. Almost all code is shared though.

CompatContainerSerializer

/**
 * The compatible serializer doesn't have access to state to determine a proper delegate format. This
 * implementation uses the default format instance. It is perfectly valid however to create a custom
 * instance (for example providing a SerialModule) here and return that from both delegate methods
 */
object CompatContainerSerializer: CommonContainerSerializer() {
    override fun delegateFormat(decoder: Decoder) = XML.defaultInstance
    override fun delegateFormat(encoder: Encoder) = XML.defaultInstance
}

ContainerSerializer

/**
 * This version of the serializer uses the new delegateFormat method on [XML.XmlInput] and [XML.XmlOutput]
 * to inherit configuration and serializerModules.
 */
object ContainerSerializer: CommonContainerSerializer() {
    override fun delegateFormat(decoder: Decoder) = (decoder as XML.XmlInput).delegateFormat()
    override fun delegateFormat(encoder: Encoder) = (encoder as XML.XmlOutput).delegateFormat()
}

CommonContainerSerializer

/**
 * A common base class that contains the actual code needed to serialize/deserialize the container.
 */
abstract class CommonContainerSerializer: KSerializer<Container> {
    /** We need to have the serializer for the elements */
    private val elementSerializer = serializer<TestElement>()

    /** Autogenerated descriptors don't work correctly here. */
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Container") {
        element("data", ListSerializer(elementSerializer).descriptor)
    }

    override fun deserialize(decoder: Decoder): Container {
        // XmlInput is designed as an interface to test for to allow custom serializers
        if (decoder is XML.XmlInput) { // We treat XML different, using a separate method for clarity
            return deserializeDynamic(decoder, decoder.input)
        } else { // Simple default decoder implementation that delegates parsing the data to the ListSerializer
            val data = decoder.decodeStructure(descriptor) {
                decodeSerializableElement(descriptor, 0, ListSerializer(elementSerializer))
            }
            return Container(data)
        }
    }

    /**
     * This function is the meat to deserializing the container with dynamic tag names. Note that
     * because we use xml there is no point in going through the (anonymous) list dance. Doing that
     * would be an additional complication.
     */
    fun deserializeDynamic(decoder: Decoder, reader: XmlReader): Container {
        val xml = delegateFormat(decoder) // get the format for deserializing

        // We need the descriptor for the element. xmlDescriptor returns a rootDescriptor, so the actual descriptor is
        // its (only) child.
        val elementXmlDescriptor = xml.xmlDescriptor(elementSerializer).getElementDescriptor(0)

        // A list to collect the data
        val dataList = mutableListOf<TestElement>()

        decoder.decodeStructure(descriptor) {
            // Finding the children is actually not left to the serialization framework, but
            // done by "hand"
            while (reader.next() != EventType.END_ELEMENT) {
                when (reader.eventType) {
                    EventType.COMMENT,
                    EventType.IGNORABLE_WHITESPACE -> {
                        // Comments and whitespace are just ignored
                    }
                    EventType.ENTITY_REF,
                    EventType.TEXT                 -> if (reader.text.isNotBlank()) {
                        // Some parsers can return whitespace as text instead of ignorable whitespace

                        // Use the handler from the configuration to throw the exception.
                        xml.config.unknownChildHandler(reader, InputKind.Text, null, emptyList())
                    }
                    // It's best to still check the name before parsing
                    EventType.START_ELEMENT        -> if(reader.namespaceURI.isEmpty() && reader.localName.startsWith("Test_")) {
                        // When reading the child tag we use the DynamicTagReader to present normalized XML to the
                        // deserializer for elements
                        val filter = DynamicTagReader(reader, elementXmlDescriptor)

                        // The test element can now be decoded as normal (with the filter applied)
                        val testElement = xml.decodeFromReader(elementSerializer, filter)
                        dataList.add(testElement)
                    } else { // handling unexpected tags
                        xml.config.unknownChildHandler(reader, InputKind.Element, reader.name, listOf("Test_??"))
                    }
                    else                           -> { // other content that shouldn't happen
                        throw XmlException("Unexpected tag content")
                    }
                }
            }
        }
        return Container(dataList)
    }

    override fun serialize(encoder: Encoder, value: Container) {
        if (encoder is XML.XmlOutput) { // When we are using the xml format use the serializeDynamic method
            return serializeDynamic(encoder, encoder.target, value.data)
        } else { // Otherwise just manually do the encoding that would have been generated
            encoder.encodeStructure(descriptor) {
                encodeSerializableElement(descriptor, 0, ListSerializer(elementSerializer), value.data)
            }
        }
    }

    /**
     * This function provides the actual dynamic serialization
     */
    fun serializeDynamic(encoder: Encoder, target: XmlWriter, data: List<TestElement>) {
        val xml = delegateFormat(encoder) // get the format for deserializing

        // We need the descriptor for the element. xmlDescriptor returns a rootDescriptor, so the actual descriptor is
        // its (only) child.
        val elementXmlDescriptor = xml.xmlDescriptor(elementSerializer).getElementDescriptor(0)

        encoder.encodeStructure(descriptor) { // create the structure (will write the tags of Container)
            for (element in data) { // write each element
                // We need a writer that does the renaming from the normal format to the dynamic format
                // It is passed the string of the id to add.
                val writer = DynamicTagWriter(target, elementXmlDescriptor, element.id.toString())

                // Normal delegate writing of the element
                xml.encodeToWriter(writer, elementSerializer, element)
            }
        }
    }

    // These functions abstract away getting the delegate format in improved or compat way.
    abstract fun delegateFormat(decoder: Decoder): XML
    abstract fun delegateFormat(encoder: Encoder): XML
}

This is supported by two filters (one for reading and writing).

DynamicTagReader

/**
 * A filter that reads xml with dynamic tags and represents it as a structured xml with id attribute
 */
internal class DynamicTagReader(reader: XmlReader, descriptor: XmlDescriptor) : XmlDelegatingReader(reader) {
    private var initDepth = reader.depth
    private val filterDepth: Int
        /**
         * We want to be safe so only handle the content at relative depth 0. The way that depth is determined
         * means that the depth is the depth after the tag (and end tags are thus one level lower than the tag (and its
         * content). We correct for that here.
         */
        get() = when (eventType) {
            EventType.END_ELEMENT -> delegate.depth - initDepth + 1
            else                  -> delegate.depth - initDepth
        }

    /**
     * Store the tag name that we need to use instead of the dynamic tag
     */
    private val elementName = descriptor.tagName

    /**
     * Store the name of the id attribute that is synthetically generated. This property is initialised
     * this way to allow for name remapping in the format policy.
     */
    private val idAttrName = (0 until descriptor.elementsCount)
        .first { descriptor.serialDescriptor.getElementName(it) == "id" }
        .let { descriptor.getElementDescriptor(it) }
        .tagName

    /**
     * This filter is created when we are at the local tag. So we can already determine the value of the
     * synthetic id property. In this case just by removing the prefix.
     */
    val idValue = delegate.localName.removePrefix("Test_")

    /**
     * When we are at relative depth 0 we add an attribute at position 0 (easier than at the end). This allows
     * for other attributes (actually written) on the tag.
     */
    override val attributeCount: Int
        get() = when (filterDepth) {
            0 -> super.attributeCount + 1
            else -> super.attributeCount
        }

    /**
     * At relative depth 0, attribute 0 we inject the namespace for the id attribute. The other attribute indices are just
     * adjusted.
     */
    override fun getAttributeNamespace(index: Int): String = when (filterDepth) {
        0 -> when (index) {
            0    -> idAttrName.namespaceURI
            else -> super.getAttributeNamespace(index - 1)
        }
        else -> super.getAttributeNamespace(index)
    }

    /**
     * At relative depth 0, attribute 0 we inject the prefix for the id attribute. The other attribute indices are just
     * adjusted.
     */
    override fun getAttributePrefix(index: Int): String = when (filterDepth) {
        0 -> when (index) {
            0    -> idAttrName.prefix
            else -> super.getAttributePrefix(index - 1)
        }
        else -> super.getAttributePrefix(index)
    }

    /**
     * At relative depth 0, attribute 0 we inject the local name for the id attribute. The other attribute indices are just
     * adjusted.
     */
    override fun getAttributeLocalName(index: Int): String = when (filterDepth) {
        0 -> when (index) {
            0    -> idAttrName.localPart
            else -> super.getAttributeLocalName(index - 1)
        }
        else -> super.getAttributeLocalName(index)
    }

    /**
     * At relative depth 0, attribute 0 we inject the value for the id attribute. The other attribute indices are just
     * adjusted.
     */
    override fun getAttributeValue(index: Int): String = when (filterDepth) {
        0 -> when (index) {
            0    -> idValue
            else -> super.getAttributeValue(index - 1)
        }
        else -> super.getAttributeValue(index)
    }

    /**
     * When attribute values are retrieved by name, pick up the synthetic id attribute at relative depth 0.
     * Note that while the xml format does not use this method it is good practice to override it anyway.
     */
    override fun getAttributeValue(nsUri: String?, localName: String): String? = when {
        filterDepth == 0 &&
                (nsUri ?: "") == idAttrName.namespaceURI &&
                localName == idAttrName.localPart -> idValue
        else                                      -> super.getAttributeValue(nsUri, localName)
    }

    /**
     * When we are at relative depth 0 we return the synthetic name rather than the original.
     */
    override val namespaceURI: String
        get() = when (filterDepth) {
            0 -> elementName.namespaceURI
            else -> super.namespaceURI
        }

    /**
     * When we are at relative depth 0 we return the synthetic name rather than the original.
     */
    override val localName: String
        get() = when (filterDepth) {
            0 -> elementName.localPart
            else -> super.localName
        }

    /**
     * When we are at relative depth 0 we return the synthetic name rather than the original.
     */
    override val prefix: String
        get() = when (filterDepth) {
            0 -> elementName.prefix
            else -> super.prefix
        }
}

DynamicTagWriter

/**
 * This filter takes the writing of the proper tag and replaces it with writing the dynamic tag. It
 * also ignores the writing of the id attribute. The id attribute is passed as parameter so it is not
 * necessary to delay writing the tag until the id attribute has been written (which, while possible,
 * introduces a lot of complexity which is unneeded in this case).
 */
internal class DynamicTagWriter(private val writer: XmlWriter, descriptor: XmlDescriptor, private val idValue: String) :
    XmlDelegatingWriter(writer) {
    private val initDepth = writer.depth
    private val filterDepth: Int
        /**
         * We want to be safe so only handle the content at relative depth 0. The way that depth is determined
         * means that the depth is the depth after the tag (and end tags are thus one level lower than the tag (and its
         * content). We correct for that here.
         */
        get() = writer.depth - initDepth

    private val idAttrName = (0 until descriptor.elementsCount)
        .first { descriptor.serialDescriptor.getElementName(it) == "id" }
        .let { descriptor.getElementDescriptor(it) }
        .tagName

    /**
     * When writing a start tag, if we are at relative depth 0 we write the dynamic tag instead of the
     * default. Otherwise we just pass along the request directly to the parent.
     */
    override fun startTag(namespace: String?, localName: String, prefix: String?) {
        when (filterDepth) {
            0    -> super.startTag("", "Test_$idValue", "")
            else -> super.startTag(namespace, localName, prefix)
        }
    }

    /**
     * Once we have written the start tag we are at depth 1. In that case ignore the id attribute.
     */
    override fun attribute(namespace: String?, name: String, prefix: String?, value: String) {
        when {
            filterDepth == 1 &&
                    (namespace ?: "") == idAttrName.namespaceURI &&
                    name == idAttrName.localPart
                 -> Unit
            else -> super.attribute(namespace, name, prefix, value)
        }

    }

    /**
     * Also fix the end tag to actually write the dynamic name.
     */
    override fun endTag(namespace: String?, localName: String, prefix: String?) {
        when (filterDepth) {
            1    -> super.endTag("", "Test_$idValue", "")
            else -> super.endTag(namespace, localName, prefix)
        }
    }
}

Example usage

main.kt

/**
 * This example shows how a custom serializer together with a filter can be used to support non-xml xml documents
 * where tag names are dynamic/unique. This example is a solution to the question in #41.
 *
 * There are 2 versions, one is the CompatContainerSerializer. This version works on 0.80.0 and 0.80.1 but has
 * limitations in that it cannot inherit configuration or serializerModules. The improved version uses new properties
 * in the XML.XmlInput and XML.XmlOutput interfaces that allow new xml serializers to be created based on the
 * configuration of the encoder/decoder.
 */
fun main() {
    /*
     * Some test data that is used for both versions of the serializer.
     */
    val testElements = listOf(
        TestElement(123, 42, "someData"),
        TestElement(456, 71, "moreData")
                             )

    // Execute the example code for the compatible serializer
    println("# Compatible")
    compat(testElements)

    // Execute the example code for the improved serializer
    println()
    println("# Improved version")
    newExample(testElements)
}

private fun compat(testElements: List<TestElement>) {
    val data = Container(testElements)

    // Instead of using the serializer for the type we use the custom one. In normal cases there would only be one
    // serializer
    val serializer = CompatContainerSerializer

    /*
     * Set an indent here to show that it is not effective (as the serialization of the child does not have access to
     * the configuration).
     */
    val xml = XML { indent = 2 }

    // Encode and print the output of serialization
    val string = xml.encodeToString(serializer, data)
    println("StringEncodingCompat:\n${string.prependIndent("    ")}")

    // Parse and print the result of deserialization
    val deserializedData = xml.decodeFromString(serializer, string)
    println("Deserialized container:\n  $deserializedData")
}

/** This example works with master, but not with the released version. */
private fun newExample(testElements: List<TestElement>) {
    val data = Container(testElements)
    val serializer = serializer<Container>() // use the default serializer

    // Create the configuration for (de)serialization
    val xml = XML { indent = 2 }

    // Encode and print the output of serialization
    val string = xml.encodeToString(serializer, data)
    println("StringEncodingCompat:\n${string.prependIndent("    ")}")

    // Parse and print the result of deserialization
    val deserializedData = xml.decodeFromString(serializer, string)
    println("Deserialized container:\n  $deserializedData")

}