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
)
}
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)])])))])