Skip to content

Commit

Permalink
Add support for alternate relative link on Atom feeds
Browse files Browse the repository at this point in the history
  • Loading branch information
prof18 committed Jan 2, 2025
1 parent 1910992 commit 165c226
Show file tree
Hide file tree
Showing 18 changed files with 119 additions and 30 deletions.
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
android-gradle-plugin = "8.7.3"
android-gradle-plugin = "8.2.2"
android-compile-sdk = "34"
android-min-sdk = "21"
android-target-sdk = "34"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ internal class AndroidXmlParser(
if (xmlPullParser.contains(RssKeyword.Rss)) {
rssChannel = extractRSSContent(xmlPullParser)
} else if (xmlPullParser.contains(AtomKeyword.Atom)) {
rssChannel = extractAtomContent(xmlPullParser)
rssChannel = extractAtomContent(xmlPullParser, input)
}
}
eventType = xmlPullParser.next()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.prof18.rssparser.internal.atom

import com.prof18.rssparser.internal.AtomKeyword
import com.prof18.rssparser.internal.ChannelFactory
import com.prof18.rssparser.internal.ParserInput
import com.prof18.rssparser.internal.attributeValue
import com.prof18.rssparser.internal.contains
import com.prof18.rssparser.internal.nextTrimmedText
Expand All @@ -30,6 +31,7 @@ import org.xmlpull.v1.XmlPullParserException

internal fun CoroutineScope.extractAtomContent(
xmlPullParser: XmlPullParser,
input: ParserInput,
): RssChannel {
val channelFactory = ChannelFactory()

Expand All @@ -41,7 +43,6 @@ internal fun CoroutineScope.extractAtomContent(

// Start parsing the xml
loop@ while (eventType != XmlPullParser.END_DOCUMENT && isActive) {

// Start parsing the item
when {
eventType == XmlPullParser.START_TAG -> when {
Expand Down Expand Up @@ -154,10 +155,19 @@ internal fun CoroutineScope.extractAtomContent(
val rel = xmlPullParser.attributeValue(
AtomKeyword.Link.Rel
)
val link = if (input.baseUrl != null &&
rel == AtomKeyword.Link.Rel.Alternate.value &&
// Some feeds have full links
href?.startsWith("/") == true
) {
input.baseUrl + href
} else {
href
}
if (rel != AtomKeyword.Link.Edit.value && rel != AtomKeyword.Link.Self.value) {
when {
insideItem -> channelFactory.articleBuilder.link(href)
else -> channelFactory.channelBuilder.link(href)
insideItem -> channelFactory.articleBuilder.link(link)
else -> channelFactory.channelBuilder.link(link)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ import kotlin.coroutines.resumeWithException
internal class IosXmlFetcher(
private val nsUrlSession: NSURLSession,
) : XmlFetcher {

override suspend fun fetchXml(url: String): ParserInput = suspendCancellableCoroutine { continuation ->
val nsUrl = NSURL(string = url)
val baseUrl = nsUrl.scheme + "://" + nsUrl.host
val task = nsUrlSession.dataTaskWithURL(
url = NSURL(string = url)
url = nsUrl
) { data: NSData?, response: NSURLResponse?, error: NSError? ->
if (error != null) {
val throwable = Throwable(
Expand All @@ -36,7 +37,7 @@ internal class IosXmlFetcher(
)
continuation.resumeWithException(exception)
} else if (data != null) {
continuation.resume(ParserInput(data))
continuation.resume(ParserInput(data, baseUrl))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal class IosXmlParser(
val parser = NSXMLParser(stream)
suspendCancellableCoroutine { continuation ->
parser.delegate = NSXMLParserDelegate(
baseFeedUrl = input.baseUrl,
onEnd = { rssChannel ->
stream.close()
continuation.resume(rssChannel)
Expand Down Expand Up @@ -60,6 +61,7 @@ internal class IosXmlParser(
}

private class NSXMLParserDelegate(
private val baseFeedUrl: String?,
private val onEnd: (RssChannel) -> Unit,
private val onError: (NSXMLParsingException) -> Unit,
) : NSObject(), NSXMLParserDelegateProtocol {
Expand All @@ -80,7 +82,7 @@ private class NSXMLParserDelegate(
}

AtomKeyword.Atom.value -> {
feedHandler = AtomFeedHandler()
feedHandler = AtomFeedHandler(baseFeedUrl)
}

else -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ package com.prof18.rssparser.internal
import platform.Foundation.NSData

internal actual data class ParserInput(
val data: NSData
val data: NSData,
val baseUrl: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import com.prof18.rssparser.internal.FeedHandler
import com.prof18.rssparser.internal.getValueOrNull
import com.prof18.rssparser.model.RssChannel

internal class AtomFeedHandler : FeedHandler {
internal class AtomFeedHandler(
private val baseFeedUrl: String?,
) : FeedHandler {

private var currentElement: String? = null

Expand All @@ -28,18 +30,28 @@ internal class AtomFeedHandler : FeedHandler {
if (isInsideItem) {
val category = attributes.getValueOrNull(
AtomKeyword.Entry.Term.value,
) as? String
) as? String
channelFactory.articleBuilder.addCategory(category)
}
}

AtomKeyword.Link.value -> {
val href = attributes.getValueOrNull(AtomKeyword.Link.Href.value) as? String
val rel = attributes.getValueOrNull(AtomKeyword.Link.Rel.value) as? String
val link = if (
baseFeedUrl != null &&
rel == AtomKeyword.Link.Rel.Alternate.value &&
// Some feeds have full links
href?.startsWith("/") == true
) {
baseFeedUrl + href
} else {
href
}
if (rel != AtomKeyword.Link.Edit.value && rel != AtomKeyword.Link.Self.value) {
when {
isInsideItem -> channelFactory.articleBuilder.link(href)
else -> channelFactory.channelBuilder.link(href)
isInsideItem -> channelFactory.articleBuilder.link(link)
else -> channelFactory.channelBuilder.link(link)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ import platform.posix.getenv

@OptIn(ExperimentalForeignApi::class)
internal actual fun readFileFromResources(
resourceName: String
resourceName: String,
): ParserInput {
val s = getenv("TEST_RESOURCES_ROOT")?.toKString()
val path = "$s/${resourceName}"
val data = NSData.dataWithContentsOfFile(path)
return ParserInput(requireNotNull(data))
return ParserInput(requireNotNull(value = data), baseUrl = BASE_FEED_URL)
}

@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
internal actual fun readFileFromResourcesAsString(
resourceName: String
resourceName: String,
): String {
val s = getenv("TEST_RESOURCES_ROOT")?.toKString()
val path = "$s/${resourceName}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ internal class JvmXmlFetcher(

override suspend fun fetchXml(url: String): ParserInput {
val request = createRequest(url)
val baseUrl = request.url.scheme + "://" + request.url.host
return ParserInput(
inputStream = callFactory.newCall(request).awaitForInputStream()
inputStream = callFactory.newCall(request).awaitForInputStream(),
baseUrl = baseUrl,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ package com.prof18.rssparser.internal
import java.io.InputStream

internal actual data class ParserInput(
val inputStream: InputStream
val inputStream: InputStream,
val baseUrl: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ internal actual fun readFileFromResources(
val path = System.getenv("TEST_RESOURCES_ROOT")
val file = File("$path/$resourceName")
return ParserInput(
inputStream = FileInputStream(file)
inputStream = FileInputStream(file),
baseUrl = BASE_FEED_URL,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ package com.prof18.rssparser.internal
internal sealed class AtomKeyword(val value: String) {
data object Atom : AtomKeyword("feed")
data object Title : AtomKeyword("title")
data object Icon: AtomKeyword("icon")
data object Icon : AtomKeyword("icon")
data object Link : AtomKeyword("link") {
data object Href : AtomKeyword("href")
data object Rel : AtomKeyword("rel")
data object Rel : AtomKeyword("rel") {
data object Alternate : AtomKeyword("alternate")
}
data object Edit : AtomKeyword("edit")
data object Self : AtomKeyword("self")
}

data object Subtitle : AtomKeyword("subtitle")
data object Updated: AtomKeyword("updated")
data object Updated : AtomKeyword("updated")
data object Entry {
data object Item : AtomKeyword("entry")
data object Guid : AtomKeyword("id")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ internal expect fun readFileFromResourcesAsString(
): String

internal expect val currentTarget: CurrentTarget

internal const val BASE_FEED_URL = "https://www.base-feed-url.com"
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.prof18.rssparser.atom

import com.prof18.rssparser.BASE_FEED_URL
import com.prof18.rssparser.BaseXmlParserTest

class XmlParserAtomRelativeLink : BaseXmlParserTest(
feedPath = "atom-relative-url.xml",
channelTitle = "Aurimas Liutikas",
channelLink = "$BASE_FEED_URL/",
channelDescription = "A personal page of Aurimas Liutikas.",
channelLastBuildDate = "2024-12-19T06:13:47+00:00",
articleTitle = "What The Distribution - What Is Inside Gradle Distribution Zips?",
articlePubDate = "2024-12-18T00:00:00+00:00",
articleLink = "$BASE_FEED_URL/2024/12/18/What-The-Distribution.html",
articleGuid = "/2024/12/18/What-The-Distribution",
articleDescription = "When I saw this post",
articleAuthor = "",
)
22 changes: 22 additions & 0 deletions rssparser/src/commonTest/resources/atom-relative-url.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<feed>
<generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator>
<link href="/feed.xml" rel="self" type="application/atom+xml"/>
<link href="/" rel="alternate" type="text/html"/>
<updated>2024-12-19T06:13:47+00:00</updated>
<id>/feed.xml</id>
<title type="html">Aurimas Liutikas</title>
<subtitle>A personal page of Aurimas Liutikas. </subtitle>
<entry>
<title type="html">
What The Distribution - What Is Inside Gradle Distribution Zips?
</title>
<link href="/2024/12/18/What-The-Distribution.html" rel="alternate" type="text/html" title="What The Distribution - What Is Inside Gradle Distribution Zips?"/>
<published>2024-12-18T00:00:00+00:00</published>
<updated>2024-12-18T00:00:00+00:00</updated>
<id>/2024/12/18/What-The-Distribution</id>
<author>
<name/>
</author>
<summary type="html">When I saw this post</summary>
</entry>
</feed>
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal class JvmXmlParser(
override suspend fun parseXML(input: ParserInput): RssChannel = withContext(dispatcher) {
try {
val parser = SAXParserFactory.newInstance().newSAXParser()
val handler = SaxFeedHandler()
val handler = SaxFeedHandler(input.baseUrl)

if (charset != null) {
val inputSource = InputSource(input.inputStream).apply {
Expand All @@ -46,11 +46,14 @@ internal class JvmXmlParser(
override fun generateParserInputFromString(rawRssFeed: String): ParserInput {
val cleanedXml = rawRssFeed.trim()
val inputStream: InputStream = cleanedXml.byteInputStream(charset ?: Charsets.UTF_8)
return ParserInput(inputStream)
// TODO: changeme
return ParserInput(inputStream, "TODO")
}
}

private class SaxFeedHandler : DefaultHandler() {
private class SaxFeedHandler (
private val feedUrl: String?,
) : DefaultHandler() {
private var feedHandler: FeedHandler? = null
private val textBuilder: StringBuilder = StringBuilder()

Expand All @@ -70,7 +73,7 @@ private class SaxFeedHandler : DefaultHandler() {
feedHandler = RssFeedHandler()
}
AtomKeyword.Atom.value -> {
feedHandler = AtomFeedHandler()
feedHandler = AtomFeedHandler(feedUrl)
}
else -> feedHandler?.onStartRssElement(qName, attributes)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import com.prof18.rssparser.internal.FeedHandler
import com.prof18.rssparser.model.RssChannel
import org.xml.sax.Attributes

internal class AtomFeedHandler : FeedHandler {
internal class AtomFeedHandler(
private val baseFeedUrl: String?,
) : FeedHandler {

private var isInsideItem = false
private var isInsideChannel = true
Expand All @@ -29,9 +31,18 @@ internal class AtomFeedHandler : FeedHandler {
val href = attributes?.getValue(AtomKeyword.Link.Href.value)
val rel = attributes?.getValue(AtomKeyword.Link.Rel.value)
if (rel != AtomKeyword.Link.Edit.value && rel != AtomKeyword.Link.Self.value) {
val link = if (baseFeedUrl != null &&
rel == AtomKeyword.Link.Rel.Alternate.value &&
// Some feeds have full links
href?.startsWith("/") == true
) {
baseFeedUrl + href
} else {
href
}
when {
isInsideItem -> channelFactory.articleBuilder.link(href)
else -> channelFactory.channelBuilder.link(href)
isInsideItem -> channelFactory.articleBuilder.link(link)
else -> channelFactory.channelBuilder.link(link)
}
}
}
Expand Down
Binary file not shown.

0 comments on commit 165c226

Please sign in to comment.