FasterXML / jackson-dataformat-xml

Extension for Jackson JSON processor that adds support for serializing POJOs as XML (and deserializing from XML) as an alternative to JSON
Apache License 2.0
567 stars 221 forks source link

Kotlin: Should `readValues` work on a XML stream without root element? (Jackson 2.13.0) #512

Open gapag opened 2 years ago

gapag commented 2 years ago

Description

I describe the issue with short snippets of code. A self-contained example trails this post.

Versions

Using gradle, I include:

val jackson_version = 2.13.0
implementation("com.fasterxml.jackson.core:jackson-databind:$jackson_version")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:$jackson_version")

Scenario

Test excerpt

val om = XmlMapper()
val it = om.readerFor(object : TypeReference<Content>() {}).readValues<Content>(inp)
assertEquals(3, it.readAll().size)

Workarounds

  1. The most straightforward way for me to workaround this issue was to wrap the input stream with another object RootWrappedInputStream that exhausts in sequence three streams reading out:

    1. an opening root entity
    2. the original stream
    3. the closing of the root entity
      (See code at the end of this post.)
  2. I got this suggestion from Tatu in the jackson-user google group:

    But what you would probably need to do would be to construct FromXmlReader (subtype of JsonParser) first, advance stream to the first XML element of the first value, and then construct MappingIterator from ObjectMapper (through ObjectReader).

    I was not able to make this work.

Executable code

import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import java.io.InputStream
import java.nio.charset.Charset
import kotlin.test.assertEquals

internal class RootWrappedInputStream(inp: InputStream) : InputStream() {

    constructor(s: String, charset: Charset = Charsets.UTF_8) : this(s.byteInputStream(charset))

    private val root = "INJECTED>"

    private val streams = mutableListOf("<$root".byteInputStream(), inp, "</$root".byteInputStream())

    override fun read(): Int {
        if (streams.isEmpty()) {
            return -1
        } else {
            val q = streams.first().read()
            if (q == -1) {
                streams.removeFirst()
                return read()
            } else {
                return q
            }
        }
    }
}

val problematicXMLUnwrapped = """
    <Content>
        <a>i1</a>
        <b>1</b>    
    </Content>    
    <Content>
        <a>i2</a>
        <b>2</b>
    </Content>
    <Content>
        <a>i3</a>
        <b>3</b>
    </Content>""".trimIndent()

data class Content(val a: String, val b: Int)

fun ObjectMapper.initMapper(): ObjectMapper {
    val kotmod = KotlinModule.Builder().build()
    registerModule(kotmod)
    return this
}

fun withReadValues(om: ObjectMapper, inp: InputStream): Set<Content> {
    val result = mutableSetOf<Content>()
    val it = om.readerFor(object : TypeReference<Content>() {}).readValues<Content>(inp)

    try {
        it.readAll(result)
        assertEquals(3, result.size)
        println("Ok")
    } catch (t: Throwable) {
        println("Error")
        throw t
    }
    return result
}

fun main() {
    val xm = XmlMapper().initMapper()
    val jm = ObjectMapper().initMapper()

    // Both succeed as per readValues' specification. No input-specific configuration required
    withReadValues(jm, """[{"a":"i1", "b":1},{"a":"i2", "b":2},{"a":"i3", "b":2}]""".byteInputStream())

    withReadValues(jm, """{"a":"i1", "b":1}{"a":"i2", "b":2}{"a":"i3", "b":2}""".byteInputStream())

    // Succeeds because there is a root element
    withReadValues(xm, "<BBB>$problematicXMLUnwrapped</BBB>".trimIndent().byteInputStream())

    // Succeeds because RootWrappedInputStream wraps a root item around the stream
    withReadValues(xm, RootWrappedInputStream(problematicXMLUnwrapped.byteInputStream()))

    // Fails as there is no root element
    withReadValues(xm, problematicXMLUnwrapped.byteInputStream())

}