summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/opml/Opml.kt
blob: 65ba1bfb09da6d484074731e720abc6693ba74bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package org.terst.feedreader.opml

import android.util.Xml
import org.terst.feedreader.data.FeedEntity
import org.xmlpull.v1.XmlPullParser
import java.io.InputStream
import java.io.OutputStream

data class OpmlFeed(val title: String, val xmlUrl: String, val htmlUrl: String?, val folder: String? = null)

/**
 * OPML import/export. Folder nesting (<outline> containing other <outline>
 * elements instead of an xmlUrl) is captured as a single folder name per
 * feed -- the nearest enclosing container outline's title/text. Nested
 * folders-within-folders collapse to just the innermost name; this app
 * groups by one folder level, not a full category tree.
 */
object Opml {

    fun parse(input: InputStream): List<OpmlFeed> {
        val parser = Xml.newPullParser()
        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
        parser.setInput(input, null)

        val feeds = mutableListOf<OpmlFeed>()
        // One entry pushed per <outline> START_TAG, popped on its END_TAG -- a folder
        // container's entry holds its name, a feed (or any other) outline's is null.
        // The current folder is whichever named entry is nearest the top of the stack.
        val outlineStack = mutableListOf<String?>()
        var eventType = parser.eventType
        while (eventType != XmlPullParser.END_DOCUMENT) {
            when (eventType) {
                XmlPullParser.START_TAG -> if (parser.name.equals("outline", ignoreCase = true)) {
                    val xmlUrl = parser.getAttributeValue(null, "xmlUrl")
                    if (xmlUrl != null) {
                        val title = parser.getAttributeValue(null, "title")
                            ?: parser.getAttributeValue(null, "text")
                            ?: xmlUrl
                        val htmlUrl = parser.getAttributeValue(null, "htmlUrl")
                        val folder = outlineStack.filterNotNull().lastOrNull()
                        feeds += OpmlFeed(title, xmlUrl, htmlUrl, folder)
                        outlineStack += null
                    } else {
                        val folderName = parser.getAttributeValue(null, "text")
                            ?: parser.getAttributeValue(null, "title")
                        outlineStack += folderName
                    }
                }
                XmlPullParser.END_TAG -> if (parser.name.equals("outline", ignoreCase = true) && outlineStack.isNotEmpty()) {
                    outlineStack.removeAt(outlineStack.size - 1)
                }
            }
            eventType = parser.next()
        }
        return feeds
    }

    fun write(feeds: List<FeedEntity>, output: OutputStream) {
        val serializer = Xml.newSerializer()
        serializer.setOutput(output, "UTF-8")
        serializer.startDocument("UTF-8", true)
        serializer.startTag(null, "opml")
        serializer.attribute(null, "version", "2.0")

        serializer.startTag(null, "head")
        serializer.startTag(null, "title")
        serializer.text("Feeds subscriptions")
        serializer.endTag(null, "title")
        serializer.endTag(null, "head")

        serializer.startTag(null, "body")
        fun writeFeedOutline(feed: FeedEntity) {
            serializer.startTag(null, "outline")
            serializer.attribute(null, "text", feed.title)
            serializer.attribute(null, "title", feed.title)
            serializer.attribute(null, "type", "rss")
            serializer.attribute(null, "xmlUrl", feed.id)
            feed.siteUrl?.let { serializer.attribute(null, "htmlUrl", it) }
            serializer.endTag(null, "outline")
        }
        val (grouped, ungrouped) = feeds.partition { it.folder != null }
        for ((folder, feedsInFolder) in grouped.groupBy { it.folder }) {
            serializer.startTag(null, "outline")
            serializer.attribute(null, "text", folder)
            serializer.attribute(null, "title", folder)
            for (feed in feedsInFolder) writeFeedOutline(feed)
            serializer.endTag(null, "outline")
        }
        for (feed in ungrouped) writeFeedOutline(feed)
        serializer.endTag(null, "body")

        serializer.endTag(null, "opml")
        serializer.endDocument()
        serializer.flush()
    }
}