Search code examples
androidkotlinretrofit2simple-framework

Kotlin - parsing xml inline list with simpleXML


I'm trying to parse an rss feed using simplexml in kotlin.

The feed is the top podcasts feed from itunes

The data returned has this structure:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Top Audio Podcasts</title>
    <link>https://rss.itunes.apple.com/api/v1/us/podcasts/top-podcasts/all/10/explicit.rss</link>
    <lastBuildDate>Fri, 13 Jul 2018 01:36:19 -0700</lastBuildDate>
    <atom:link rel="self" type="application/rss+xml" href="https://rss.itunes.apple.com/api/v1/us/podcasts/top-podcasts/all/10/explicit.rss"/>
    <atom:link rel="alternate" type="text/html" href="https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewTop?genreId=26&amp;popId=28"/>
    <description>iTunes Store : Top Audio Podcasts</description>
    <category>iTunes Store</category>
    <copyright>Copyright © 2018 Apple Inc. All rights reserved.</copyright>
    <item>
      <title>The Wilderness - Crooked Media</title>
      <category domain="">Crooked Media</category>
      <link>https://itunes.apple.com/us/podcast/the-wilderness/id1408796715?mt=2</link>
      <guid>https://itunes.apple.com/us/podcast/the-wilderness/id1408796715?mt=2</guid>
      <description>The Wilderness</description>
      <category>podcast</category>
      <category>News &amp; Politics</category>
      <category>Podcasts</category>
      <category>Society &amp; Culture</category>
      <category>History</category>
      <pubDate>Fri, 6 Jul 2018 00:00:00 +0000</pubDate>
    </item>
    <item>
      <title>Aaron Mahnke's Cabinet of Curiosities - HowStuffWorks &amp; Aaron Mahnke</title>
      <category domain="https://itunes.apple.com/us/artist/howstuffworks-aaron-mahnke/284341002?mt=2">HowStuffWorks &amp; Aaron Mahnke</category>
      <link>https://itunes.apple.com/us/podcast/aaron-mahnkes-cabinet-of-curiosities/id1396546917?mt=2</link>
      <guid>https://itunes.apple.com/us/podcast/aaron-mahnkes-cabinet-of-curiosities/id1396546917?mt=2</guid>
      <description>Aaron Mahnke's Cabinet of Curiosities</description>
      <category>podcast</category>
      <category>History</category>
      <category>Podcasts</category>
      <category>Society &amp; Culture</category>
      <pubDate>Tue, 10 Jul 2018 00:00:00 +0000</pubDate>
    </item>
  </channel>
</rss>

The portion I care about is the list of podcasts contained by the item tags.

The model classes I have look like this:

@Root(name = "channel", strict = false)
data class Chart @JvmOverloads constructor(
        @field:ElementList(entry = "item", inline = true)
        @param:ElementList(entry = "item", inline = true)
        val podcasts: List<Entry>? = null)

@Root(name = "item", strict = false)
data class Entry @JvmOverloads constructor(
        @field:Element(name = "description")
        @param:Element(name = "description")
        val title: String? = null,

        @field:Element(name = "category")
        @param:Element(name = "category")
        val artist: String? = null,

        @field:Element(name = "guid")
        @param:Element(name = "guid")
        val guid: String? = null)

I have a simple unit tests that loads the xml from file, passes it to the simplexml deserialiser and then compares the output to some expected models.

At the moment when I run the test I get an exception:

org.simpleframework.xml.core.ValueRequiredException: Unable to satisfy @org.simpleframework.xml.ElementList(entry=item, inline=true, data=false, name=, type=void, required=true, empty=true) on field 'podcasts' private final java.util.List Chart.podcasts for class Chart at line 2

Adding required = false to the @ElementList annotation removes this error but the deserialiser returns null for the list.

My best guess is that the deserialiser can't find the item list although from my understanding of the @ElementList, using the entry field should allow it to find the tags.

The use case for this code is to form the network layer of an Android app that relies on Retrofit2 for it's REST calls.

Thanks for any help you can give on this.

Note: The use of the @Param and @Field declaration was to combat a different exception, I got the idea from this question


Solution

  • I've found a solution.

    So the first issue was that my root element wasn't the Chart as that represented the channel tag, I needed a wrapper class which I've called ChartFeed to represent the rss tag.

    I realised this by adding an element for the title tag on the Chart object which was also unfindable.

    My finished data classes look like this:

    @Root(name = "rss", strict = false)
    data class ChartFeed(
            @field:Element(name = "channel")
            @param:Element(name = "channel")
            val chart: Chart? = null)
    
    @Root(name = "channel", strict = false)
    data class Chart @JvmOverloads constructor(
            @field:ElementList(entry = "item", inline = true)
            @param:ElementList(entry = "item", inline = true)
            val podcasts: List<Entry>? = null)
    
    @Root(name = "item", strict = false)
    data class Entry @JvmOverloads constructor(
            @field:Path("description")
            @field:Text(required = false)
            @param:Path("description")
            @param:Text(required = false)
            val title: String? = null,
    
            @field:Path("category")
            @field:Text(required = false)
            @param:Path("category")
            @param:Text(required = false)
            val artist: String? = null,
    
            @field:Element(name = "guid")
            @param:Element(name = "guid")
            val guid: String? = null)
    

    I've changed the @Element tag for a few fields/params to a combination of @Path and @Text, this was because both the channel and item xml objects reuse the same tags which was causing this exception:

    org.simpleframework.xml.core.PersistenceException: Element 'category' is already used with @org.simpleframework.xml.Element(name=category, type=void, data=false, required=true) on field 'artist' private final java.lang.String Entry.artist at line 18
    

    The solution I used is detailed here