mirror of https://github.com/readrops/Readrops.git
Replace SImpleXML with KonsumeXML and KotlinXMLBuilder
This commit is contained in:
parent
4c86b17043
commit
71df7a7a31
|
@ -56,6 +56,7 @@ dependencies {
|
|||
testImplementation "io.insert-koin:koin-test:$rootProject.ext.koin_version"
|
||||
|
||||
implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:1.0'
|
||||
implementation 'org.redundent:kotlin-xml-builder:1.7.3'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
|
||||
|
||||
|
@ -66,7 +67,6 @@ dependencies {
|
|||
exclude group: 'com.squareup.moshi', module: 'moshi'
|
||||
}
|
||||
|
||||
implementation 'com.squareup.retrofit2:converter-simplexml:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
|
||||
|
||||
implementation 'com.squareup.moshi:moshi:1.12.0'
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
package com.readrops.api.opml
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import java.lang.Exception
|
||||
|
||||
class OPMLAdapter : XmlAdapter<Map<Folder?, List<Feed>>> {
|
||||
|
||||
override fun fromXml(konsumer: Konsumer): Map<Folder?, List<Feed>> = try {
|
||||
var opml: Map<Folder?, List<Feed>>? = null
|
||||
|
||||
konsumer.child("opml") {
|
||||
val version = attributes.getValueOrNull("version")
|
||||
|
||||
if (version != "2.0")
|
||||
throw ParseException("Only 2.0 OPML is supported")
|
||||
|
||||
allChildrenAutoIgnore(Names.of("body")) {
|
||||
opml = parseOutline(this)
|
||||
}
|
||||
}
|
||||
|
||||
opml!!
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse outline and its children recursively
|
||||
* @param konsumer
|
||||
*/
|
||||
private fun parseOutline(konsumer: Konsumer): MutableMap<Folder?, MutableList<Feed>> = with(konsumer) {
|
||||
val opml = mutableMapOf<Folder?, MutableList<Feed>>()
|
||||
|
||||
children(Names.of("outline")) {
|
||||
val title = attributes.getValueOrNull("title")
|
||||
?: attributes.getValueOrNull("text")
|
||||
|
||||
val xmlUrl = attributes.getValueOrNull("xmlUrl")
|
||||
val htmlUrl = attributes.getValueOrNull("htmlUrl")
|
||||
|
||||
val recursiveOpml = parseOutline(this)
|
||||
|
||||
// The outline is a folder/category
|
||||
if (recursiveOpml.containsKey(null) || xmlUrl.isNullOrEmpty()) {
|
||||
// if the outline doesn't have text or title value but contains sub outlines,
|
||||
// those sub outlines will be considered as not belonging to any folder and join the others at the top level of the hierarchy
|
||||
val folder = if (title != null) Folder(name = title) else null
|
||||
|
||||
val feeds = recursiveOpml[null] ?: mutableListOf()
|
||||
opml += mapOf(folder to feeds)
|
||||
|
||||
recursiveOpml.remove(null)
|
||||
opml += recursiveOpml
|
||||
} else { // the outline is a feed
|
||||
val feed = Feed().apply {
|
||||
name = title
|
||||
url = xmlUrl
|
||||
siteUrl = htmlUrl
|
||||
}
|
||||
|
||||
// parsed feed is linked to null to be assigned to the previous level folder
|
||||
if (opml.containsKey(null)) {
|
||||
opml[null]?.plusAssign(feed)
|
||||
} else
|
||||
opml += mapOf(null to mutableListOf(feed))
|
||||
}
|
||||
}
|
||||
|
||||
return opml
|
||||
}
|
||||
}
|
|
@ -1,32 +1,24 @@
|
|||
package com.readrops.api.opml
|
||||
|
||||
import com.readrops.api.opml.model.Body
|
||||
import com.readrops.api.opml.model.Head
|
||||
import com.readrops.api.opml.model.OPML
|
||||
import com.readrops.api.opml.model.Outline
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
import org.simpleframework.xml.Serializer
|
||||
import org.simpleframework.xml.core.Persister
|
||||
import org.redundent.kotlin.xml.xml
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
object OPMLParser {
|
||||
|
||||
val TAG: String = OPMLParser.javaClass.simpleName
|
||||
|
||||
@JvmStatic
|
||||
fun read(stream: InputStream): Single<Map<Folder?, List<Feed>>> {
|
||||
return Single.create { emitter ->
|
||||
try {
|
||||
val serializer: Serializer = Persister()
|
||||
val adapter = OPMLAdapter()
|
||||
val opml = adapter.fromXml(stream.konsumeXml())
|
||||
|
||||
val opml: OPML = serializer.read(OPML::class.java, stream)
|
||||
|
||||
emitter.onSuccess(opmlToFoldersAndFeeds(opml))
|
||||
emitter.onSuccess(opml)
|
||||
} catch (e: Exception) {
|
||||
emitter.onError(e)
|
||||
}
|
||||
|
@ -36,115 +28,47 @@ object OPMLParser {
|
|||
@JvmStatic
|
||||
fun write(foldersAndFeeds: Map<Folder?, List<Feed>>, outputStream: OutputStream): Completable {
|
||||
return Completable.create { emitter ->
|
||||
val serializer: Serializer = Persister()
|
||||
serializer.write(foldersAndFeedsToOPML(foldersAndFeeds), outputStream)
|
||||
val opml = xml("opml") {
|
||||
attribute("version", "2.0")
|
||||
|
||||
"head" {
|
||||
-"Subscriptions"
|
||||
}
|
||||
|
||||
"body" {
|
||||
for (folderAndFeeds in foldersAndFeeds) {
|
||||
if (folderAndFeeds.key != null) { // feeds with folder
|
||||
"outline" {
|
||||
folderAndFeeds.key?.name?.let {
|
||||
attribute("title", it)
|
||||
attribute("text", it)
|
||||
}
|
||||
|
||||
for (feed in folderAndFeeds.value) {
|
||||
"outline" {
|
||||
feed.name?.let { attribute("title", it) }
|
||||
attribute("xmlUrl", feed.url!!)
|
||||
feed.siteUrl?.let { attribute("htmlUrl", it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (feed in folderAndFeeds.value) { // feeds without folder
|
||||
"outline" {
|
||||
feed.name?.let { attribute("title", it) }
|
||||
attribute("xmlUrl", feed.url!!)
|
||||
feed.siteUrl?.let { attribute("htmlUrl", it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outputStream.write(opml.toString().toByteArray())
|
||||
outputStream.flush()
|
||||
|
||||
emitter.onComplete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun opmlToFoldersAndFeeds(opml: OPML): Map<Folder?, List<Feed>> {
|
||||
if (opml.version != "2.0")
|
||||
throw ParseException("Only 2.0 OPML specification is supported")
|
||||
|
||||
val foldersAndFeeds: MutableMap<Folder?, List<Feed>> = HashMap()
|
||||
val body = opml.body!!
|
||||
|
||||
body.outlines?.forEach { outline ->
|
||||
val outlineParsing = parseOutline(outline)
|
||||
associateOrphanFeedsToFolder(foldersAndFeeds, outlineParsing, null)
|
||||
|
||||
foldersAndFeeds.putAll(outlineParsing)
|
||||
}
|
||||
|
||||
return foldersAndFeeds
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse outline and its children recursively
|
||||
* @param outline node to parse
|
||||
*/
|
||||
private fun parseOutline(outline: Outline): MutableMap<Folder?, List<Feed>> {
|
||||
val foldersAndFeeds: MutableMap<Folder?, List<Feed>> = HashMap()
|
||||
|
||||
// The outline is a folder/category
|
||||
if ((outline.outlines != null && !outline.outlines?.isEmpty()!!) || outline.xmlUrl.isNullOrEmpty()) {
|
||||
// if the outline doesn't have text or title value but contains sub outlines,
|
||||
// those sub outlines will be considered as not belonging to any folder and join the others at the top level of the hierarchy
|
||||
val folder = if (outline.name != null) Folder(name = outline.name) else null
|
||||
|
||||
outline.outlines?.forEach {
|
||||
val recursiveFeedsFolders = parseOutline(it)
|
||||
|
||||
// Treat feeds without folder, so belonging to the current folder
|
||||
associateOrphanFeedsToFolder(foldersAndFeeds, recursiveFeedsFolders, folder)
|
||||
foldersAndFeeds.putAll(recursiveFeedsFolders.toMap())
|
||||
}
|
||||
|
||||
// empty outline
|
||||
if (!foldersAndFeeds.containsKey(folder)) foldersAndFeeds[folder] = listOf()
|
||||
|
||||
} else { // the outline is a feed
|
||||
if (!outline.xmlUrl.isNullOrEmpty()) {
|
||||
val feed = Feed().apply {
|
||||
name = outline.name
|
||||
url = outline.xmlUrl
|
||||
siteUrl = outline.htmlUrl
|
||||
}
|
||||
// parsed feed is linked to null to be assigned to the previous level folder
|
||||
foldersAndFeeds[null] = listOf(feed)
|
||||
}
|
||||
}
|
||||
|
||||
return foldersAndFeeds
|
||||
}
|
||||
|
||||
private fun foldersAndFeedsToOPML(foldersAndFeeds: Map<Folder?, List<Feed>>): OPML {
|
||||
val outlines = arrayListOf<Outline>()
|
||||
|
||||
for (folderAndFeeds in foldersAndFeeds) {
|
||||
if (folderAndFeeds.key != null) {
|
||||
val outline = Outline(folderAndFeeds.key?.name)
|
||||
|
||||
val feedOutlines = arrayListOf<Outline>()
|
||||
for (feed in folderAndFeeds.value) {
|
||||
val feedOutline = Outline(feed.name, feed.url!!, feed.siteUrl)
|
||||
|
||||
feedOutlines += feedOutline
|
||||
}
|
||||
|
||||
outline.outlines = feedOutlines
|
||||
outlines += outline
|
||||
} else {
|
||||
for (feed in folderAndFeeds.value) {
|
||||
outlines += Outline(feed.name, feed.url!!, feed.siteUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val head = Head("Subscriptions")
|
||||
val body = Body(outlines)
|
||||
|
||||
return OPML("2.0", head, body)
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate parsed feeds without folder to the previous level folder.
|
||||
* @param foldersAndFeeds final result
|
||||
* @param parsingResult current level parsing
|
||||
* @param folder the folder feeds will be associated to
|
||||
*
|
||||
*/
|
||||
private fun associateOrphanFeedsToFolder(
|
||||
foldersAndFeeds: MutableMap<Folder?, List<Feed>>,
|
||||
parsingResult: MutableMap<Folder?, List<Feed>>, folder: Folder?,
|
||||
) {
|
||||
val feeds = parsingResult[null]
|
||||
if (feeds != null && feeds.isNotEmpty()) {
|
||||
if (foldersAndFeeds[folder] == null) foldersAndFeeds[folder] = feeds
|
||||
else foldersAndFeeds[folder] = foldersAndFeeds[folder]?.plus(feeds)!!
|
||||
|
||||
parsingResult.remove(null)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package com.readrops.api.opml.model
|
||||
|
||||
import org.simpleframework.xml.ElementList
|
||||
import org.simpleframework.xml.Root
|
||||
|
||||
@Root(name = "body", strict = false)
|
||||
data class Body(@field:ElementList(inline = true, required = true) var outlines: List<Outline>?) {
|
||||
|
||||
/**
|
||||
* empty constructor required by SimpleXMl
|
||||
*/
|
||||
constructor() : this(null)
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package com.readrops.api.opml.model
|
||||
|
||||
import org.simpleframework.xml.Element
|
||||
import org.simpleframework.xml.Root
|
||||
|
||||
@Root(name = "head", strict = false)
|
||||
data class Head(@field:Element(required = false) var title: String?) {
|
||||
|
||||
/**
|
||||
* empty constructor required by SimpleXML
|
||||
*/
|
||||
constructor() : this(null)
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package com.readrops.api.opml.model
|
||||
|
||||
import org.simpleframework.xml.Attribute
|
||||
import org.simpleframework.xml.Element
|
||||
import org.simpleframework.xml.Order
|
||||
import org.simpleframework.xml.Root
|
||||
|
||||
@Order(elements = ["head", "body"])
|
||||
@Root(name = "opml", strict = false)
|
||||
data class OPML(@field:Attribute(required = true) var version: String?,
|
||||
@field:Element(required = false) var head: Head?,
|
||||
@field:Element(required = true) var body: Body?) {
|
||||
|
||||
/**
|
||||
* empty constructor required by SimpleXML
|
||||
*/
|
||||
constructor() : this(null, null, null)
|
||||
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package com.readrops.api.opml.model
|
||||
|
||||
import org.simpleframework.xml.Attribute
|
||||
import org.simpleframework.xml.ElementList
|
||||
import org.simpleframework.xml.Root
|
||||
|
||||
@Root(name = "outline", strict = false)
|
||||
data class Outline(@field:Attribute(required = false) private var title: String?,
|
||||
@field:Attribute(required = false) private var text: String?,
|
||||
@field:Attribute(required = false) var type: String?,
|
||||
@field:Attribute(required = false) var xmlUrl: String?,
|
||||
@field:Attribute(required = false) var htmlUrl: String?,
|
||||
@field:ElementList(inline = true, required = false) var outlines: List<Outline>?) {
|
||||
|
||||
/**
|
||||
* empty constructor required by SimpleXML
|
||||
*/
|
||||
constructor() : this(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null)
|
||||
|
||||
constructor(title: String?) : this(title, title, null, null, null, null)
|
||||
|
||||
constructor(title: String?, xmlUrl: String, htmlUrl: String?) : this(title, title, "rss", xmlUrl, htmlUrl, null)
|
||||
|
||||
val name: String?
|
||||
get() = title ?: text
|
||||
}
|
Loading…
Reference in New Issue