Implement Android RSS/Atom feed parser
- Add FeedParser.kt with automatic feed type detection - Add RSSParser.kt for RSS 2.0 feeds - Add AtomParser.kt for Atom 1.0 feeds - Add comprehensive unit tests for both parsers - Support iTunes namespace and enclosures - Fix pre-existing compilation issues in the codebase - Update build.gradle.kts with proper dependencies and AGP 8.5.0
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
package com.rssuper.parsing
|
||||
|
||||
import com.rssuper.models.Enclosure
|
||||
import com.rssuper.models.Feed
|
||||
import com.rssuper.models.FeedItem
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.StringReader
|
||||
|
||||
object AtomParser {
|
||||
|
||||
private val ATOM_NS = "http://www.w3.org/2005/Atom"
|
||||
private val ITUNES_NS = "http://www.itunes.com/dtds/podcast-1.0.dtd"
|
||||
private val MEDIA_NS = "http://search.yahoo.com/mrss/"
|
||||
|
||||
fun parse(xml: String, feedUrl: String): Feed {
|
||||
val factory = XmlPullParserFactory.newInstance()
|
||||
factory.isNamespaceAware = true
|
||||
val parser = factory.newPullParser()
|
||||
parser.setInput(StringReader(xml))
|
||||
|
||||
var title: String? = null
|
||||
var link: String? = null
|
||||
var subtitle: String? = null
|
||||
var updated: java.util.Date? = null
|
||||
var generator: String? = null
|
||||
val items = mutableListOf<FeedItem>()
|
||||
|
||||
var currentItem: MutableMap<String, Any?>? = null
|
||||
var currentTag: String? = null
|
||||
var inContent = false
|
||||
|
||||
var eventType = parser.eventType
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
when (eventType) {
|
||||
XmlPullParser.START_TAG -> {
|
||||
val tagName = parser.name
|
||||
val namespace = parser.namespace
|
||||
|
||||
when {
|
||||
tagName == "feed" -> {}
|
||||
tagName == "entry" -> {
|
||||
currentItem = mutableMapOf()
|
||||
}
|
||||
tagName == "title" -> {
|
||||
currentTag = tagName
|
||||
inContent = true
|
||||
}
|
||||
tagName == "link" -> {
|
||||
val href = parser.getAttributeValue(null, "href")
|
||||
val rel = parser.getAttributeValue(null, "rel")
|
||||
if (href != null) {
|
||||
if (currentItem != null) {
|
||||
if (rel == "alternate" || rel == null) {
|
||||
currentItem["link"] = href
|
||||
} else if (rel == "enclosure") {
|
||||
val type = parser.getAttributeValue(null, "type") ?: "application/octet-stream"
|
||||
val length = parser.getAttributeValue(null, "length")?.toLongOrNull()
|
||||
currentItem["enclosure"] = Enclosure(href, type, length)
|
||||
}
|
||||
} else {
|
||||
if (rel == "alternate" || rel == null) {
|
||||
link = href
|
||||
}
|
||||
}
|
||||
}
|
||||
currentTag = null
|
||||
inContent = false
|
||||
}
|
||||
tagName == "subtitle" -> {
|
||||
currentTag = tagName
|
||||
inContent = true
|
||||
}
|
||||
tagName == "summary" -> {
|
||||
currentTag = tagName
|
||||
inContent = true
|
||||
}
|
||||
tagName == "content" -> {
|
||||
currentTag = tagName
|
||||
inContent = true
|
||||
}
|
||||
tagName == "updated" || tagName == "published" -> {
|
||||
currentTag = tagName
|
||||
inContent = true
|
||||
}
|
||||
tagName == "name" -> {
|
||||
currentTag = tagName
|
||||
inContent = true
|
||||
}
|
||||
tagName == "uri" -> {
|
||||
currentTag = tagName
|
||||
inContent = true
|
||||
}
|
||||
tagName == "id" -> {
|
||||
currentTag = tagName
|
||||
inContent = true
|
||||
}
|
||||
tagName == "category" -> {
|
||||
val term = parser.getAttributeValue(null, "term")
|
||||
if (term != null && currentItem != null) {
|
||||
val cats = currentItem["categories"] as? MutableList<String> ?: mutableListOf()
|
||||
cats.add(term)
|
||||
currentItem["categories"] = cats
|
||||
}
|
||||
currentTag = null
|
||||
inContent = false
|
||||
}
|
||||
tagName == "generator" -> {
|
||||
currentTag = tagName
|
||||
inContent = true
|
||||
}
|
||||
tagName == "summary" && namespace == ITUNES_NS -> {
|
||||
if (currentItem != null) {
|
||||
currentItem["itunesSummary"] = readElementText(parser)
|
||||
}
|
||||
}
|
||||
tagName == "image" && namespace == ITUNES_NS -> {
|
||||
val href = parser.getAttributeValue(null, "href")
|
||||
if (href != null && currentItem != null) {
|
||||
currentItem["image"] = href
|
||||
}
|
||||
}
|
||||
tagName == "duration" && namespace == ITUNES_NS -> {
|
||||
currentItem?.put("duration", readElementText(parser))
|
||||
}
|
||||
tagName == "thumbnail" && namespace == MEDIA_NS -> {
|
||||
val url = parser.getAttributeValue(null, "url")
|
||||
if (url != null && currentItem != null) {
|
||||
currentItem["mediaThumbnail"] = url
|
||||
}
|
||||
}
|
||||
tagName == "enclosure" && namespace == MEDIA_NS -> {
|
||||
val url = parser.getAttributeValue(null, "url")
|
||||
val type = parser.getAttributeValue(null, "type")
|
||||
val length = parser.getAttributeValue(null, "length")?.toLongOrNull()
|
||||
if (url != null && type != null && currentItem != null) {
|
||||
currentItem["enclosure"] = Enclosure(url, type, length)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
XmlPullParser.TEXT -> {
|
||||
val text = parser.text?.xmlTrimmed() ?: ""
|
||||
if (text.isNotEmpty() && inContent) {
|
||||
if (currentItem != null) {
|
||||
when (currentTag) {
|
||||
"title" -> currentItem["title"] = text
|
||||
"summary" -> currentItem["summary"] = text
|
||||
"content" -> currentItem["content"] = text
|
||||
"name" -> currentItem["author"] = text
|
||||
"id" -> currentItem["guid"] = text
|
||||
"updated", "published" -> currentItem[currentTag] = text
|
||||
}
|
||||
} else {
|
||||
when (currentTag) {
|
||||
"title" -> title = text
|
||||
"subtitle" -> subtitle = text
|
||||
"id" -> if (title == null) title = text
|
||||
"updated" -> updated = XmlDateParser.parse(text)
|
||||
"generator" -> generator = text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XmlPullParser.END_TAG -> {
|
||||
val tagName = parser.name
|
||||
if (tagName == "entry" && currentItem != null) {
|
||||
items.add(buildFeedItem(currentItem))
|
||||
currentItem = null
|
||||
}
|
||||
if (tagName == currentTag) {
|
||||
currentTag = null
|
||||
inContent = false
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = parser.next()
|
||||
}
|
||||
|
||||
return Feed(
|
||||
id = generateUuid(),
|
||||
title = title ?: "Untitled Feed",
|
||||
link = link,
|
||||
subtitle = subtitle,
|
||||
description = subtitle,
|
||||
updated = updated,
|
||||
generator = generator,
|
||||
items = items,
|
||||
rawUrl = feedUrl,
|
||||
lastFetchedAt = java.util.Date()
|
||||
)
|
||||
}
|
||||
|
||||
private fun readElementText(parser: XmlPullParser): String {
|
||||
var text = ""
|
||||
var eventType = parser.eventType
|
||||
while (eventType != XmlPullParser.END_TAG) {
|
||||
if (eventType == XmlPullParser.TEXT) {
|
||||
text = parser.text.xmlDecoded()
|
||||
}
|
||||
eventType = parser.next()
|
||||
}
|
||||
return text.xmlTrimmed()
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun buildFeedItem(item: Map<String, Any?>): FeedItem {
|
||||
val title = item["title"] as? String ?: "Untitled"
|
||||
val link = item["link"] as? String
|
||||
val summary = item["summary"] as? String
|
||||
val content = item["content"] as? String ?: summary
|
||||
val itunesSummary = item["itunesSummary"] as? String
|
||||
val author = item["author"] as? String
|
||||
val guid = item["guid"] as? String ?: link ?: generateUuid()
|
||||
val categories = item["categories"] as? List<String>
|
||||
val enclosure = item["enclosure"] as? Enclosure
|
||||
|
||||
val updatedStr = item["updated"] as? String
|
||||
val publishedStr = item["published"] as? String
|
||||
val published = XmlDateParser.parse(publishedStr ?: updatedStr)
|
||||
val updated = XmlDateParser.parse(updatedStr)
|
||||
|
||||
return FeedItem(
|
||||
id = generateUuid(),
|
||||
title = title,
|
||||
link = link,
|
||||
description = summary ?: itunesSummary,
|
||||
content = content,
|
||||
author = author,
|
||||
published = published,
|
||||
updated = updated,
|
||||
categories = categories,
|
||||
enclosure = enclosure,
|
||||
guid = guid
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.rssuper.parsing
|
||||
|
||||
import com.rssuper.models.Feed
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.StringReader
|
||||
import java.util.Date
|
||||
|
||||
object FeedParser {
|
||||
|
||||
fun parse(xml: String, feedUrl: String): ParseResult {
|
||||
val feedType = detectFeedType(xml)
|
||||
|
||||
return when (feedType) {
|
||||
FeedType.RSS -> {
|
||||
val feed = RSSParser.parse(xml, feedUrl)
|
||||
ParseResult(FeedType.RSS, feed)
|
||||
}
|
||||
FeedType.Atom -> {
|
||||
val feed = AtomParser.parse(xml, feedUrl)
|
||||
ParseResult(FeedType.Atom, feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun parseAsync(xml: String, feedUrl: String, callback: (Result<ParseResult>) -> Unit) {
|
||||
try {
|
||||
val result = parse(xml, feedUrl)
|
||||
callback(Result.success(result))
|
||||
} catch (e: Exception) {
|
||||
callback(Result.failure(e))
|
||||
}
|
||||
}
|
||||
|
||||
private fun detectFeedType(xml: String): FeedType {
|
||||
val factory = XmlPullParserFactory.newInstance()
|
||||
factory.isNamespaceAware = true
|
||||
val parser = factory.newPullParser()
|
||||
parser.setInput(StringReader(xml))
|
||||
|
||||
var eventType = parser.eventType
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
val tagName = parser.name
|
||||
return when {
|
||||
tagName.equals("rss", ignoreCase = true) -> FeedType.RSS
|
||||
tagName.equals("feed", ignoreCase = true) -> FeedType.Atom
|
||||
tagName.equals("RDF", ignoreCase = true) -> FeedType.RSS
|
||||
else -> {
|
||||
val namespace = parser.namespace
|
||||
if (namespace != null && namespace.isNotEmpty()) {
|
||||
when {
|
||||
tagName.equals("rss", ignoreCase = true) -> FeedType.RSS
|
||||
tagName.equals("feed", ignoreCase = true) -> FeedType.Atom
|
||||
else -> throw FeedParsingError.UnsupportedFeedType
|
||||
}
|
||||
} else {
|
||||
throw FeedParsingError.UnsupportedFeedType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = parser.next()
|
||||
}
|
||||
throw FeedParsingError.UnsupportedFeedType
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.rssuper.parsing
|
||||
|
||||
sealed class FeedType(val value: String) {
|
||||
data object RSS : FeedType("rss")
|
||||
data object Atom : FeedType("atom")
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String): FeedType {
|
||||
return when (value.lowercase()) {
|
||||
"rss" -> RSS
|
||||
"atom" -> Atom
|
||||
else -> throw IllegalArgumentException("Unknown feed type: $value")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.rssuper.parsing
|
||||
|
||||
import com.rssuper.models.Feed
|
||||
|
||||
data class ParseResult(
|
||||
val feedType: FeedType,
|
||||
val feed: Feed
|
||||
)
|
||||
|
||||
sealed class FeedParsingError : Exception() {
|
||||
data object UnsupportedFeedType : FeedParsingError()
|
||||
data object MalformedXml : FeedParsingError()
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.rssuper.parsing
|
||||
|
||||
import com.rssuper.models.Enclosure
|
||||
import com.rssuper.models.Feed
|
||||
import com.rssuper.models.FeedItem
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.StringReader
|
||||
import java.util.Date
|
||||
|
||||
object RSSParser {
|
||||
|
||||
private val ITUNES_NS = "http://www.itunes.com/dtds/podcast-1.0.dtd"
|
||||
private val CONTENT_NS = "http://purl.org/rss/1.0/modules/content/"
|
||||
|
||||
fun parse(xml: String, feedUrl: String): Feed {
|
||||
val factory = XmlPullParserFactory.newInstance()
|
||||
factory.isNamespaceAware = true
|
||||
val parser = factory.newPullParser()
|
||||
parser.setInput(StringReader(xml))
|
||||
|
||||
var title: String? = null
|
||||
var link: String? = null
|
||||
var description: String? = null
|
||||
var language: String? = null
|
||||
var lastBuildDate: Date? = null
|
||||
var generator: String? = null
|
||||
var ttl: Int? = null
|
||||
val items = mutableListOf<FeedItem>()
|
||||
|
||||
var currentItem: MutableMap<String, Any?>? = null
|
||||
var currentTag: String? = null
|
||||
|
||||
var eventType = parser.eventType
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
when (eventType) {
|
||||
XmlPullParser.START_TAG -> {
|
||||
val tagName = parser.name
|
||||
val namespace = parser.namespace
|
||||
|
||||
when {
|
||||
tagName == "channel" -> {}
|
||||
tagName == "item" -> {
|
||||
currentItem = mutableMapOf()
|
||||
}
|
||||
tagName == "title" || tagName == "description" ||
|
||||
tagName == "link" || tagName == "author" ||
|
||||
tagName == "guid" || tagName == "pubDate" ||
|
||||
tagName == "category" || tagName == "enclosure" -> {
|
||||
currentTag = tagName
|
||||
}
|
||||
tagName == "language" -> currentTag = tagName
|
||||
tagName == "lastBuildDate" -> currentTag = tagName
|
||||
tagName == "generator" -> currentTag = tagName
|
||||
tagName == "ttl" -> currentTag = tagName
|
||||
|
||||
tagName == "subtitle" && namespace == ITUNES_NS -> {
|
||||
if (currentItem == null) {
|
||||
description = readElementText(parser)
|
||||
}
|
||||
}
|
||||
tagName == "summary" && namespace == ITUNES_NS -> {
|
||||
currentItem?.put("description", readElementText(parser))
|
||||
}
|
||||
tagName == "duration" && namespace == ITUNES_NS -> {
|
||||
currentItem?.put("duration", readElementText(parser))
|
||||
}
|
||||
tagName == "image" && namespace == ITUNES_NS -> {
|
||||
val href = parser.getAttributeValue(null, "href")
|
||||
if (href != null && currentItem != null) {
|
||||
currentItem.put("image", href)
|
||||
}
|
||||
}
|
||||
tagName == "encoded" && namespace == CONTENT_NS -> {
|
||||
currentItem?.put("content", readElementText(parser))
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
if (tagName == "enclosure" && currentItem != null) {
|
||||
val url = parser.getAttributeValue(null, "url")
|
||||
val type = parser.getAttributeValue(null, "type")
|
||||
val length = parser.getAttributeValue(null, "length")?.toLongOrNull()
|
||||
if (url != null && type != null) {
|
||||
currentItem["enclosure"] = Enclosure(url, type, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XmlPullParser.TEXT -> {
|
||||
val text = parser.text?.xmlTrimmed() ?: ""
|
||||
if (text.isNotEmpty()) {
|
||||
if (currentItem != null) {
|
||||
when (currentTag) {
|
||||
"title" -> currentItem["title"] = text
|
||||
"description" -> currentItem["description"] = text
|
||||
"link" -> currentItem["link"] = text
|
||||
"author" -> currentItem["author"] = text
|
||||
"guid" -> currentItem["guid"] = text
|
||||
"pubDate" -> currentItem["pubDate"] = text
|
||||
"category" -> {
|
||||
val cats = currentItem["categories"] as? MutableList<String> ?: mutableListOf()
|
||||
cats.add(text)
|
||||
currentItem["categories"] = cats
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when (currentTag) {
|
||||
"title" -> title = text
|
||||
"link" -> link = text
|
||||
"description" -> description = text
|
||||
"language" -> language = text
|
||||
"lastBuildDate" -> lastBuildDate = XmlDateParser.parse(text)
|
||||
"generator" -> generator = text
|
||||
"ttl" -> ttl = text.toIntOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XmlPullParser.END_TAG -> {
|
||||
val tagName = parser.name
|
||||
if (tagName == "item" && currentItem != null) {
|
||||
items.add(buildFeedItem(currentItem))
|
||||
currentItem = null
|
||||
}
|
||||
currentTag = null
|
||||
}
|
||||
}
|
||||
eventType = parser.next()
|
||||
}
|
||||
|
||||
return Feed(
|
||||
id = generateUuid(),
|
||||
title = title ?: "Untitled Feed",
|
||||
link = link,
|
||||
description = description,
|
||||
language = language,
|
||||
lastBuildDate = lastBuildDate,
|
||||
generator = generator,
|
||||
ttl = ttl,
|
||||
items = items,
|
||||
rawUrl = feedUrl,
|
||||
lastFetchedAt = Date()
|
||||
)
|
||||
}
|
||||
|
||||
private fun readElementText(parser: XmlPullParser): String {
|
||||
var text = ""
|
||||
var eventType = parser.eventType
|
||||
while (eventType != XmlPullParser.END_TAG) {
|
||||
if (eventType == XmlPullParser.TEXT) {
|
||||
text = parser.text.xmlDecoded()
|
||||
}
|
||||
eventType = parser.next()
|
||||
}
|
||||
return text.xmlTrimmed()
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun buildFeedItem(item: Map<String, Any?>): FeedItem {
|
||||
val title = item["title"] as? String ?: "Untitled"
|
||||
val link = item["link"] as? String
|
||||
val description = item["description"] as? String
|
||||
val content = item["content"] as? String ?: description
|
||||
val author = item["author"] as? String
|
||||
val guid = item["guid"] as? String ?: link ?: generateUuid()
|
||||
val categories = item["categories"] as? List<String>
|
||||
val enclosure = item["enclosure"] as? Enclosure
|
||||
|
||||
val pubDateStr = item["pubDate"] as? String
|
||||
val published = XmlDateParser.parse(pubDateStr)
|
||||
|
||||
return FeedItem(
|
||||
id = generateUuid(),
|
||||
title = title,
|
||||
link = link,
|
||||
description = description,
|
||||
content = content,
|
||||
author = author,
|
||||
published = published,
|
||||
updated = published,
|
||||
categories = categories,
|
||||
enclosure = enclosure,
|
||||
guid = guid
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.rssuper.parsing
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.util.UUID
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object XmlDateParser {
|
||||
private val iso8601WithFractional: SimpleDateFormat by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
}
|
||||
|
||||
private val iso8601: SimpleDateFormat by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
}
|
||||
|
||||
private val dateFormats: List<SimpleDateFormat> by lazy {
|
||||
listOf(
|
||||
SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US),
|
||||
SimpleDateFormat("EEE, dd MMM yyyy HH:mm Z", Locale.US),
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US),
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US),
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US),
|
||||
SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
).map {
|
||||
SimpleDateFormat(it.toPattern(), Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun parse(value: String?): java.util.Date? {
|
||||
val trimmed = value?.xmlTrimmed() ?: return null
|
||||
if (trimmed.isEmpty()) return null
|
||||
|
||||
return try {
|
||||
iso8601WithFractional.parse(trimmed)
|
||||
} catch (e: Exception) {
|
||||
try {
|
||||
iso8601.parse(trimmed)
|
||||
} catch (e: Exception) {
|
||||
for (format in dateFormats) {
|
||||
try {
|
||||
return format.parse(trimmed)
|
||||
} catch (e: Exception) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.xmlTrimmed(): String = this.trim { it <= ' ' }
|
||||
|
||||
fun String.xmlNilIfEmpty(): String? {
|
||||
val trimmed = this.xmlTrimmed()
|
||||
return if (trimmed.isEmpty()) null else trimmed
|
||||
}
|
||||
|
||||
fun String.xmlDecoded(): String {
|
||||
return this
|
||||
.replace(Regex("<!\\[CDATA\\[", RegexOption.IGNORE_CASE), "")
|
||||
.replace(Regex("\\]\\]>", RegexOption.IGNORE_CASE), "")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("&", "&")
|
||||
.replace(""", "\"")
|
||||
.replace("'", "'")
|
||||
.replace("'", "'")
|
||||
.replace("'", "'")
|
||||
}
|
||||
|
||||
fun xmlInt64(value: String?): Long? {
|
||||
val trimmed = value?.xmlTrimmed() ?: return null
|
||||
if (trimmed.isEmpty()) return null
|
||||
return trimmed.toLongOrNull()
|
||||
}
|
||||
|
||||
fun xmlInt(value: String?): Int? {
|
||||
val trimmed = value?.xmlTrimmed() ?: return null
|
||||
if (trimmed.isEmpty()) return null
|
||||
return trimmed.toIntOrNull()
|
||||
}
|
||||
|
||||
fun xmlFirstTagValue(tag: String, inXml: String): String? {
|
||||
val pattern = Pattern.compile("(?is)<(?:\\w+:)?$tag\\b[^>]*>(.*?)</(?:\\w+:)?$tag}>", Pattern.CASE_INSENSITIVE)
|
||||
val matcher = pattern.matcher(inXml)
|
||||
return if (matcher.find()) {
|
||||
matcher.group(1)?.xmlDecoded()?.xmlTrimmed()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun xmlAllTagValues(tag: String, inXml: String): List<String> {
|
||||
val pattern = Pattern.compile("(?is)<(?:\\w+:)?$tag\\b[^>]*>(.*?)</(?:\\w+:)?$tag}>", Pattern.CASE_INSENSITIVE)
|
||||
val matcher = pattern.matcher(inXml)
|
||||
val results = mutableListOf<String>()
|
||||
while (matcher.find()) {
|
||||
matcher.group(1)?.xmlDecoded()?.xmlTrimmed()?.let { value ->
|
||||
if (value.isNotEmpty()) {
|
||||
results.add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
fun xmlFirstBlock(tag: String, inXml: String): String? {
|
||||
val pattern = Pattern.compile("(?is)<(?:\\w+:)?$tag\\b[^>]*>(.*?)</(?:\\w+:)?$tag}>", Pattern.CASE_INSENSITIVE)
|
||||
val matcher = pattern.matcher(inXml)
|
||||
return if (matcher.find()) matcher.group(1) else null
|
||||
}
|
||||
|
||||
fun xmlAllBlocks(tag: String, inXml: String): List<String> {
|
||||
val pattern = Pattern.compile("(?is)<(?:\\w+:)?$tag\\b[^>]*>(.*?)</(?:\\w+:)?$tag}>", Pattern.CASE_INSENSITIVE)
|
||||
val matcher = pattern.matcher(inXml)
|
||||
val results = mutableListOf<String>()
|
||||
while (matcher.find()) {
|
||||
matcher.group(1)?.let { results.add(it) }
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
fun xmlAllTagAttributes(tag: String, inXml: String): List<Map<String, String>> {
|
||||
val pattern = Pattern.compile("(?is)<(?:\\w+:)?$tag\\b([^>]*)/?>", Pattern.CASE_INSENSITIVE)
|
||||
val matcher = pattern.matcher(inXml)
|
||||
val results = mutableListOf<Map<String, String>>()
|
||||
while (matcher.find()) {
|
||||
matcher.group(1)?.let { results.add(parseXmlAttributes(it)) }
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
private fun parseXmlAttributes(raw: String): Map<String, String> {
|
||||
val pattern = Pattern.compile("(\\w+(?::\\w+)?)\\s*=\\s*\"([^\"]*)\"")
|
||||
val matcher = pattern.matcher(raw)
|
||||
val result = mutableMapOf<String, String>()
|
||||
while (matcher.find()) {
|
||||
val key = matcher.group(1)?.lowercase() ?: continue
|
||||
val value = matcher.group(2)?.xmlDecoded()?.xmlTrimmed() ?: continue
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun generateUuid(): String = UUID.randomUUID().toString()
|
||||
Reference in New Issue
Block a user