Search code examples
kotlinretrofitsimple-framework

Kotlin - parsing Xml response with list using SimpleXml


I'm trying to parse XML response from API and have a problem with list of "Field" elements. I'm trying to create Field object with attributes and text inside the element like properties of this object and can't find out what annotation I need to use for text of element. I tried to use

  @get:Text
  @set:Text

and

@field:Text

but got the same error

java.lang.RuntimeException: org.simpleframework.xml.core.PersistenceException: Constructor not matched for class NetworkTestContainer$Field

Can anyone advise me an appropriate annotation for this case, please?

There is XML structure:

<response>
<commands>
    <command>
        <nick>QUEUECAUSES_LIST</nick>
        <result>
            <DATASET Version="1.0" Class="TQueryAdv" Name="">
                <Row Index="1">
                    <Field Name="SHOPID" Type="6" Size="0">-1</Field>
                    <Field Name="IDCODE" Type="6" Size="0">3000000000001</Field>
                    <Field Name="CAPTION" Type="1" Size="255">Консультация</Field>
                    <Field Name="PRIORITY" Type="6" Size="0">0</Field>
                </Row>
                <Row Index="2">
                    <Field Name="SHOPID" Type="6" Size="0">-1</Field>
                    <Field Name="IDCODE" Type="6" Size="0">3000000000021</Field>
                    <Field Name="CAPTION" Type="1" Size="255">Очередь</Field>
                    <Field Name="PRIORITY" Type="6" Size="0">1</Field>
                </Row>
            </DATASET>
        </result>
    </command>
</commands>
</response>

Here are Models

@Root(name = "response", strict = false)
data class NetworkTestContainer(
    @field:ElementList(name = "commands")
    val commands : List<Command>
) {
    @Root(name = "command", strict = false)
    data class Command(
        @field:Element
        val nick : String,
        @field:Element(name = "result")
        val result : Result
    )

    @Root(name = "result", strict = false)
    data class Result(
        @field:Element(name = "DATASET")
        val dataSet: DataSet
    )

    @Root(name = "DATASET", strict = false)
    data class DataSet(
        @field:Attribute(name = "Version")
        val version : String,

        @field:Attribute(name = "Class")
        val className : String,

        @field:Attribute(name = "Name")
        val Name : String,

        @field:ElementList(inline = true)
        val rows : List<Row>
    )

    @Root(name = "Row", strict = false)
    data class Row(
        @field:Attribute(name = "Index")
        val index : Int,

        @field:ElementList(inline = true, entry = "Field")
        val fields: List<Field>

//        @field:ElementMap(attribute = true, entry = "Field", key = "Name", inline = true)
//        val fields : Map<String, String>
    )

    @Root(name = "Field", strict = false)
    data class Field (
        @field:Attribute(name = "Name", required = false)
        val key : String,

        @field:Attribute(name = "Type", required = false)
        val type : Int,

        @field:Attribute(name = "Size", required = false)
        val size : Int,

        @get:Text
        @set:Text
        var text : String
    )
}

Solution

  • I'm not familiar with this library, but I tried debugging it and here are my findings.

    According to the documentation, this library works by either using an empty constructor with field/getter/setter annotation or by using constructor injection + getter annotation.

    In the first case you would need to get rid of your data modifier and use @field: annotation, plus lateinit var or other approaches to initialise all fields when creating the object. Example:

    import org.simpleframework.xml.Attribute
    import org.simpleframework.xml.Element
    import org.simpleframework.xml.ElementList
    import org.simpleframework.xml.Root
    import org.simpleframework.xml.core.Persister
    
    
    @Root(name = "response", strict = false)
    class NetworkTestContainer {
        @field:ElementList(name = "commands")
        lateinit var commands: List<Command>
        override fun toString(): String {
            return "NetworkTestContainer(commands=$commands)"
        }
    
    }
    
    @Root(name = "command", strict = false)
    class Command {
        @field:Element(name = "nick")
        lateinit var nick: String
    
        @field:Element(name = "result")
        lateinit var result: Result
        override fun toString(): String {
            return "Command(nick='$nick', result=$result)"
        }
    
    
    }
    
    @Root(name = "result", strict = false)
    class Result {
        @field:Element(name = "DATASET")
        lateinit var dataSet: DataSet
        override fun toString(): String {
            return "Result(dataSet=$dataSet)"
        }
    
    
    }
    
    @Root(name = "DATASET", strict = false)
    class DataSet {
        @field:Attribute(name = "Version")
        lateinit var version: String
    
        @field:Attribute(name = "Class")
        lateinit var className: String
    
        @field:Attribute(name = "Name")
        lateinit var name: String
    
        @field:ElementList(inline = true)
        lateinit var rows: List<Row>
        override fun toString(): String {
            return "DataSet(version='$version', className='$className', name='$name', rows=$rows)"
        }
    
    
    }
    
    @Root(name = "Row", strict = false)
    class Row {
        @field:Attribute(name = "Index")
        var index: Int = 0
    
        @field:ElementList(inline = true, entry = "Field")
        lateinit var fields: List<Field>
        override fun toString(): String {
            return "Row(index=$index, fields=$fields)"
        }
    
    //        @field:ElementMap(attribute = true, entry = "Field", key = "Name", inline = true)
    //        val fields : Map<String, String>
    
    
    }
    
    @Root(name = "Field", strict = false)
    class Field {
        @field:Attribute(name = "Name", required = false)
        lateinit var key: String
    
        @field:Attribute(name = "Type", required = false)
        var type: Int = 0
    
        @field:Attribute(name = "Size", required = false)
        var size: Int = 0
        override fun toString(): String {
            return "Field(key='$key', type=$type, size=$size)"
        }
    
    
    }
    
    
    object Main {
    
        @JvmStatic
        fun main(args: Array<String>) {
            val deserializer = Persister()
            val file = Main::class.java.getResourceAsStream("input.xml")
            val container = deserializer.read(NetworkTestContainer::class.java, file)
            println(container)
        }
    }
    

    This prints:

    NetworkTestContainer(commands=[Command(nick='QUEUECAUSES_LIST', result=Result(dataSet=DataSet(version='1.0', className='TQueryAdv', name='', rows=[Row(index=1, fields=[Field(key='SHOPID', type=6, size=0), Field(key='IDCODE', type=6, size=0), Field(key='CAPTION', type=1, size=255), Field(key='PRIORITY', type=6, size=0)]), Row(index=2, fields=[Field(key='SHOPID', type=6, size=0), Field(key='IDCODE', type=6, size=0), Field(key='CAPTION', type=1, size=255), Field(key='PRIORITY', type=6, size=0)])])))])
    

    As you can see, this is not ideal but it works.

    A - probably - better approach would involve using constructor injection, which also requires annotated getters or fields (as specified in the documentation linked above).

    In order to do that you need to use @param: annotations (as you need to annotate constructor arguments) plus @get: annotations (as you need to annotate the getter). Example:

    import org.simpleframework.xml.Attribute
    import org.simpleframework.xml.Element
    import org.simpleframework.xml.ElementList
    import org.simpleframework.xml.Root
    import org.simpleframework.xml.Text
    import org.simpleframework.xml.core.Persister
    
    
    @Root(name = "response", strict = false)
    data class NetworkTestContainer(
            @param:ElementList(name = "commands")
            @get:ElementList(name = "commands")
            val commands: List<Command>
    ) {
        @Root(name = "command", strict = false)
        data class Command(
                @param:Element(name="nick")
                @get:Element(name="nick")
                val nick: String,
                @param:Element(name = "result")
                @get:Element(name = "result")
                val result: Result
        )
    
        @Root(name = "result", strict = false)
        data class Result(
                @param:Element(name = "DATASET")
                @get:Element(name = "DATASET")
                val dataSet: DataSet
        )
    
        @Root(name = "DATASET", strict = false)
        data class DataSet(
                @param:Attribute(name = "Version")
                @get:Attribute(name = "Version")
                val version: String,
    
                @param:Attribute(name = "Class")
                @get:Attribute(name = "Class")
                val className: String,
    
                @param:Attribute(name = "Name")
                @get:Attribute(name = "Name")
                val Name: String,
    
                @param:ElementList(inline = true)
                @get:ElementList(inline = true)
                val rows: List<Row>
        )
    
        @Root(name = "Row", strict = false)
        data class Row(
                @param:Attribute(name = "Index")
                @get:Attribute(name = "Index")
                val index: Int,
    
                @param:ElementList(inline = true, entry = "Field")
                @get:ElementList(inline = true, entry = "Field")
                val fields: List<Field>
    
    //        @field:ElementMap(attribute = true, entry = "Field", key = "Name", inline = true)
    //        val fields : Map<String, String>
        )
    
        @Root(name = "Field", strict = false)
        data class Field(
                @param:Attribute(name = "Name", required = false)
                @get:Attribute(name = "Name", required = false)
                val key: String,
    
                @param:Attribute(name = "Type", required = false)
                @get:Attribute(name = "Type", required = false)
                val type: Int,
    
                @param:Attribute(name = "Size", required = false)
                @get:Attribute(name = "Size", required = false)
                val size: Int
        )
    }
    
    object Main {
    
        @JvmStatic
        fun main(args: Array<String>) {
            val deserializer = Persister()
            val file = Main::class.java.getResourceAsStream("input.xml")
            val container = deserializer.read(NetworkTestContainer::class.java, file)
            println(container)
        }
    }
    

    This prints the same thing as above (apart from some styling differences for strings):

    NetworkTestContainer(commands=[Command(nick=QUEUECAUSES_LIST, result=Result(dataSet=DataSet(version=1.0, className=TQueryAdv, Name=, rows=[Row(index=1, fields=[Field(key=SHOPID, type=6, size=0), Field(key=IDCODE, type=6, size=0), Field(key=CAPTION, type=1, size=255), Field(key=PRIORITY, type=6, size=0)]), Row(index=2, fields=[Field(key=SHOPID, type=6, size=0), Field(key=IDCODE, type=6, size=0), Field(key=CAPTION, type=1, size=255), Field(key=PRIORITY, type=6, size=0)])])))])