mirror of https://github.com/readrops/Readrops.git
Merge branch 'feature/local_rss_memory_improvements' into develop
This commit is contained in:
commit
a3c3e7af1e
|
@ -2,8 +2,9 @@ package com.readrops.api.localfeed
|
|||
|
||||
import android.accounts.NetworkErrorException
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.localfeed.json.JSONFeedAdapter
|
||||
import com.readrops.api.localfeed.json.JSONItemsAdapter
|
||||
import com.readrops.api.utils.ApiUtils
|
||||
import com.readrops.api.utils.AuthInterceptor
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
|
@ -19,12 +20,11 @@ import okhttp3.Response
|
|||
import okio.Buffer
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.lang.Exception
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
class LocalRSSDataSource(private val httpClient: OkHttpClient): KoinComponent {
|
||||
class LocalRSSDataSource(private val httpClient: OkHttpClient) : KoinComponent {
|
||||
|
||||
/**
|
||||
* Query RSS url
|
||||
|
@ -40,27 +40,10 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient): KoinComponent {
|
|||
|
||||
return when {
|
||||
response.isSuccessful -> {
|
||||
val header = response.header(ApiUtils.CONTENT_TYPE_HEADER)
|
||||
?: throw UnknownFormatException("Unable to get $url content-type")
|
||||
|
||||
val contentType = ApiUtils.parseContentType(header)
|
||||
?: throw ParseException("Unable to parse $url content-type")
|
||||
|
||||
var type = LocalRSSHelper.getRSSType(contentType)
|
||||
|
||||
val bodyArray = response.peekBody(Long.MAX_VALUE).bytes()
|
||||
|
||||
// if we can't guess type based on content-type header, we use the content
|
||||
if (type == LocalRSSHelper.RSSType.UNKNOWN)
|
||||
type = LocalRSSHelper.getRSSContentType(ByteArrayInputStream(bodyArray))
|
||||
// if we can't guess type even with the content, we are unable to go further
|
||||
if (type == LocalRSSHelper.RSSType.UNKNOWN) throw UnknownFormatException("Unable to guess $url RSS type")
|
||||
|
||||
val feed = parseFeed(ByteArrayInputStream(bodyArray), type, response)
|
||||
val items = parseItems(ByteArrayInputStream(bodyArray), type)
|
||||
val pair = parseResponse(response, url)
|
||||
|
||||
response.body?.close()
|
||||
Pair(feed, items)
|
||||
pair
|
||||
}
|
||||
response.code == HttpURLConnection.HTTP_NOT_MODIFIED -> null
|
||||
else -> throw NetworkErrorException("$url returned ${response.code} code : ${response.message}")
|
||||
|
@ -85,8 +68,19 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient): KoinComponent {
|
|||
|
||||
var type = LocalRSSHelper.getRSSType(contentType)
|
||||
|
||||
if (type == LocalRSSHelper.RSSType.UNKNOWN)
|
||||
type = LocalRSSHelper.getRSSContentType(response.body?.byteStream()!!) // stream is closed in helper method
|
||||
if (type == LocalRSSHelper.RSSType.UNKNOWN) {
|
||||
val konsumer = response.body!!.byteStream().konsumeXml().apply {
|
||||
try {
|
||||
val rootKonsumer = nextElement(LocalRSSHelper.RSS_ROOT_NAMES)
|
||||
rootKonsumer?.let { type = LocalRSSHelper.guessRSSType(rootKonsumer) }
|
||||
} catch (e: Exception) {
|
||||
throw UnknownFormatException(e.message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
konsumer.close()
|
||||
}
|
||||
|
||||
type != LocalRSSHelper.RSSType.UNKNOWN
|
||||
} else false
|
||||
|
@ -100,53 +94,80 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient): KoinComponent {
|
|||
return httpClient.newCall(requestBuilder.build()).execute()
|
||||
}
|
||||
|
||||
private fun parseFeed(stream: InputStream, type: LocalRSSHelper.RSSType, response: Response): Feed {
|
||||
val feed = if (type != LocalRSSHelper.RSSType.JSONFEED) {
|
||||
private fun parseResponse(response: Response, url: String): Pair<Feed, List<Item>> {
|
||||
val header = response.header(ApiUtils.CONTENT_TYPE_HEADER)
|
||||
?: throw UnknownFormatException("Unable to get $url content-type")
|
||||
|
||||
val contentType = ApiUtils.parseContentType(header)
|
||||
?: throw ParseException("Unable to parse $url content-type")
|
||||
|
||||
var type = LocalRSSHelper.getRSSType(contentType)
|
||||
|
||||
var konsumer: Konsumer? = null
|
||||
if (type != LocalRSSHelper.RSSType.JSONFEED)
|
||||
konsumer = response.body!!.byteStream().konsumeXml()
|
||||
|
||||
var rootKonsumer: Konsumer? = null
|
||||
// if we can't guess type based on content-type header, we use the content
|
||||
if (type == LocalRSSHelper.RSSType.UNKNOWN) {
|
||||
try {
|
||||
konsumer = response.body!!.byteStream().konsumeXml()
|
||||
rootKonsumer = konsumer.nextElement(LocalRSSHelper.RSS_ROOT_NAMES)
|
||||
|
||||
if (rootKonsumer != null) {
|
||||
type = LocalRSSHelper.guessRSSType(rootKonsumer)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw UnknownFormatException(e.message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// if we can't guess type even with the content, we are unable to go further
|
||||
if (type == LocalRSSHelper.RSSType.UNKNOWN) throw UnknownFormatException("Unable to guess $url RSS type")
|
||||
|
||||
val pair = parseFeed(rootKonsumer ?: konsumer, type, response)
|
||||
|
||||
rootKonsumer?.finish()
|
||||
konsumer?.close()
|
||||
|
||||
return pair
|
||||
}
|
||||
|
||||
private fun parseFeed(konsumer: Konsumer?, type: LocalRSSHelper.RSSType, response: Response): Pair<Feed, List<Item>> {
|
||||
val pair = if (type != LocalRSSHelper.RSSType.JSONFEED) {
|
||||
val adapter = XmlAdapter.xmlFeedAdapterFactory(type)
|
||||
|
||||
adapter.fromXml(stream)
|
||||
adapter.fromXml(konsumer!!)
|
||||
} else {
|
||||
val adapter = Moshi.Builder()
|
||||
.add(JSONFeedAdapter())
|
||||
.build()
|
||||
.adapter(Feed::class.java)
|
||||
val pairType = Types.newParameterizedType(Pair::class.java, Feed::class.java,
|
||||
Types.newParameterizedType(List::class.java, Item::class.java))
|
||||
|
||||
adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
val adapter = Moshi.Builder()
|
||||
.add(pairType, JSONFeedAdapter())
|
||||
.build()
|
||||
.adapter<Pair<Feed, List<Item>>>(pairType)
|
||||
|
||||
adapter.fromJson(Buffer().readFrom(response.body!!.byteStream()))!!
|
||||
}
|
||||
|
||||
handleSpecialCases(feed, type, response)
|
||||
handleSpecialCases(pair.first, type, response)
|
||||
|
||||
feed.etag = response.header(ApiUtils.ETAG_HEADER)
|
||||
feed.lastModified = response.header(ApiUtils.LAST_MODIFIED_HEADER)
|
||||
pair.first.etag = response.header(ApiUtils.ETAG_HEADER)
|
||||
pair.first.lastModified = response.header(ApiUtils.LAST_MODIFIED_HEADER)
|
||||
|
||||
return feed
|
||||
return pair
|
||||
}
|
||||
|
||||
private fun parseItems(stream: InputStream, type: LocalRSSHelper.RSSType): List<Item> {
|
||||
return if (type != LocalRSSHelper.RSSType.JSONFEED) {
|
||||
val adapter = XmlAdapter.xmlItemsAdapterFactory(type)
|
||||
|
||||
adapter.fromXml(stream)
|
||||
} else {
|
||||
val adapter = Moshi.Builder()
|
||||
.add(Types.newParameterizedType(MutableList::class.java, Item::class.java), JSONItemsAdapter())
|
||||
.build()
|
||||
.adapter<List<Item>>(Types.newParameterizedType(MutableList::class.java, Item::class.java))
|
||||
|
||||
adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSpecialCases(feed: Feed, type: LocalRSSHelper.RSSType, response: Response) {
|
||||
with(feed) {
|
||||
if (type == LocalRSSHelper.RSSType.RSS_2) {
|
||||
// if an atom:link element was parsed, we still replace its value as it is unreliable,
|
||||
// otherwise we just add the rss url
|
||||
url = response.request.url.toString()
|
||||
} else if (type == LocalRSSHelper.RSSType.ATOM || type == LocalRSSHelper.RSSType.RSS_1) {
|
||||
if (url == null) url = response.request.url.toString()
|
||||
if (siteUrl == null) siteUrl = response.request.url.scheme + "://" + response.request.url.host
|
||||
private fun handleSpecialCases(feed: Feed, type: LocalRSSHelper.RSSType, response: Response) =
|
||||
with(feed) {
|
||||
if (type == LocalRSSHelper.RSSType.RSS_2) {
|
||||
// if an atom:link element was parsed, we still replace its value as it is unreliable,
|
||||
// otherwise we just add the rss url
|
||||
url = response.request.url.toString()
|
||||
} else if (type == LocalRSSHelper.RSSType.ATOM || type == LocalRSSHelper.RSSType.RSS_1) {
|
||||
if (url == null) url = response.request.url.toString()
|
||||
if (siteUrl == null) siteUrl = response.request.url.scheme + "://" + response.request.url.host
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
package com.readrops.api.localfeed
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.readrops.api.utils.extensions.checkRoot
|
||||
import java.io.InputStream
|
||||
|
||||
object LocalRSSHelper {
|
||||
|
@ -10,9 +13,10 @@ object LocalRSSHelper {
|
|||
private const val JSONFEED_CONTENT_TYPE = "application/feed+json"
|
||||
private const val JSON_CONTENT_TYPE = "application/json"
|
||||
|
||||
private const val RSS_1_REGEX = "<rdf:RDF.*xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\""
|
||||
private const val RSS_2_REGEX = "rss.*version=\"2.0\""
|
||||
private const val ATOM_REGEX = "<feed.* xmlns=\"http://www.w3.org/2005/Atom\""
|
||||
const val RSS_1_ROOT_NAME = "RDF"
|
||||
const val RSS_2_ROOT_NAME = "rss"
|
||||
const val ATOM_ROOT_NAME = "feed"
|
||||
val RSS_ROOT_NAMES = Names.of(RSS_1_ROOT_NAME, RSS_2_ROOT_NAME, ATOM_ROOT_NAME)
|
||||
|
||||
/**
|
||||
* Guess RSS type based on content-type header
|
||||
|
@ -27,33 +31,15 @@ object LocalRSSHelper {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess RSS type based on xml content
|
||||
*/
|
||||
fun getRSSContentType(content: InputStream): RSSType {
|
||||
val stringBuffer = StringBuffer()
|
||||
val reader = content.bufferedReader()
|
||||
|
||||
// we get the first 10 lines which should be sufficient to get the type,
|
||||
// otherwise iterating over the whole file could be too slow
|
||||
for (i in 0..9) stringBuffer.append(reader.readLine())
|
||||
|
||||
val string = stringBuffer.toString()
|
||||
val type = when {
|
||||
RSS_1_REGEX.toRegex().containsMatchIn(string) -> RSSType.RSS_1
|
||||
RSS_2_REGEX.toRegex().containsMatchIn(string) -> RSSType.RSS_2
|
||||
ATOM_REGEX.toRegex().containsMatchIn(string) -> RSSType.ATOM
|
||||
else -> RSSType.UNKNOWN
|
||||
}
|
||||
|
||||
reader.close()
|
||||
content.close()
|
||||
return type
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isRSSType(type: String?): Boolean {
|
||||
return if (type != null) getRSSType(type) != RSSType.UNKNOWN else false
|
||||
fun isRSSType(type: String?): Boolean =
|
||||
if (type != null) getRSSType(type) != RSSType.UNKNOWN else false
|
||||
|
||||
fun guessRSSType(konsumer: Konsumer): RSSType = when {
|
||||
konsumer.checkRoot(RSS_1_ROOT_NAME) -> RSSType.RSS_1
|
||||
konsumer.checkRoot(RSS_2_ROOT_NAME) -> RSSType.RSS_2
|
||||
konsumer.checkRoot(ATOM_ROOT_NAME) -> RSSType.ATOM
|
||||
else -> RSSType.UNKNOWN
|
||||
}
|
||||
|
||||
enum class RSSType {
|
||||
|
|
|
@ -1,35 +1,24 @@
|
|||
package com.readrops.api.localfeed
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.readrops.api.localfeed.atom.ATOMFeedAdapter
|
||||
import com.readrops.api.localfeed.atom.ATOMItemsAdapter
|
||||
import com.readrops.api.localfeed.rss1.RSS1FeedAdapter
|
||||
import com.readrops.api.localfeed.rss1.RSS1ItemsAdapter
|
||||
import com.readrops.api.localfeed.rss2.RSS2FeedAdapter
|
||||
import com.readrops.api.localfeed.rss2.RSS2ItemsAdapter
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Item
|
||||
import java.io.InputStream
|
||||
|
||||
interface XmlAdapter<T> {
|
||||
|
||||
fun fromXml(inputStream: InputStream): T
|
||||
fun fromXml(konsumer: Konsumer): T
|
||||
|
||||
companion object {
|
||||
fun xmlFeedAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter<Feed> = when (type) {
|
||||
fun xmlFeedAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter<Pair<Feed, List<Item>>> = when (type) {
|
||||
LocalRSSHelper.RSSType.RSS_1 -> RSS1FeedAdapter()
|
||||
LocalRSSHelper.RSSType.RSS_2 -> RSS2FeedAdapter()
|
||||
LocalRSSHelper.RSSType.ATOM -> ATOMFeedAdapter()
|
||||
else -> throw IllegalArgumentException("Unknown RSS type : $type")
|
||||
}
|
||||
|
||||
fun xmlItemsAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter<List<Item>> =
|
||||
when (type) {
|
||||
LocalRSSHelper.RSSType.RSS_1 -> RSS1ItemsAdapter()
|
||||
LocalRSSHelper.RSSType.RSS_2 -> RSS2ItemsAdapter()
|
||||
LocalRSSHelper.RSSType.ATOM -> ATOMItemsAdapter()
|
||||
else -> throw IllegalArgumentException("Unknown RSS type : $type")
|
||||
}
|
||||
|
||||
const val AUTHORS_MAX = 4
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,42 +3,46 @@ package com.readrops.api.localfeed.atom
|
|||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.localfeed.LocalRSSHelper
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.checkElement
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
import com.readrops.api.utils.extensions.nullableText
|
||||
import com.readrops.db.entities.Feed
|
||||
import java.io.InputStream
|
||||
import com.readrops.db.entities.Item
|
||||
|
||||
class ATOMFeedAdapter : XmlAdapter<Feed> {
|
||||
class ATOMFeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
|
||||
|
||||
override fun fromXml(inputStream: InputStream): Feed {
|
||||
val konsume = inputStream.konsumeXml()
|
||||
override fun fromXml(konsumer: Konsumer): Pair<Feed, List<Item>> {
|
||||
val feed = Feed()
|
||||
|
||||
val items = arrayListOf<Item>()
|
||||
val itemAdapter = ATOMItemAdapter()
|
||||
|
||||
return try {
|
||||
konsume.child("feed") {
|
||||
allChildrenAutoIgnore(names) {
|
||||
konsumer.checkElement(LocalRSSHelper.ATOM_ROOT_NAME) {
|
||||
it.allChildrenAutoIgnore(names) {
|
||||
with(feed) {
|
||||
when (tagName) {
|
||||
"title" -> name = nonNullText()
|
||||
"link" -> parseLink(this@allChildrenAutoIgnore, feed)
|
||||
"subtitle" -> description = nullableText()
|
||||
"entry" -> items += itemAdapter.fromXml(this@allChildrenAutoIgnore)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
konsume.close()
|
||||
feed
|
||||
konsumer.close()
|
||||
Pair(feed, items)
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLink(konsume: Konsumer, feed: Feed) = with(konsume) {
|
||||
val rel = attributes.getValueOpt("rel")
|
||||
private fun parseLink(konsumer: Konsumer, feed: Feed) = with(konsumer) {
|
||||
val rel = attributes.getValueOrNull("rel")
|
||||
|
||||
if (rel == "self")
|
||||
feed.url = attributes["href"]
|
||||
|
@ -47,6 +51,6 @@ class ATOMFeedAdapter : XmlAdapter<Feed> {
|
|||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "link", "subtitle")
|
||||
val names = Names.of("title", "link", "subtitle", "entry")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package com.readrops.api.localfeed.atom
|
||||
|
||||
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.*
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
import com.readrops.api.utils.extensions.nullableText
|
||||
import com.readrops.api.utils.extensions.nullableTextRecursively
|
||||
import com.readrops.db.entities.Item
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
class ATOMItemAdapter : XmlAdapter<Item> {
|
||||
|
||||
override fun fromXml(konsumer: Konsumer): Item {
|
||||
val item = Item()
|
||||
|
||||
return try {
|
||||
item.apply {
|
||||
konsumer.allChildrenAutoIgnore(names) {
|
||||
when (tagName) {
|
||||
"title" -> title = nonNullText()
|
||||
"id" -> guid = nullableText()
|
||||
"updated" -> pubDate = DateUtils.parse(nullableText())
|
||||
"link" -> parseLink(this, this@apply)
|
||||
"author" -> allChildrenAutoIgnore("name") { author = nullableText() }
|
||||
"summary" -> description = nullableTextRecursively()
|
||||
"content" -> content = nullableTextRecursively()
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateItem(item)
|
||||
if (item.pubDate == null) item.pubDate = LocalDateTime.now()
|
||||
if (item.guid == null) item.guid = item.link
|
||||
|
||||
item
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLink(konsumer: Konsumer, item: Item) = with(konsumer) {
|
||||
if (attributes.getValueOrNull("rel") == null ||
|
||||
attributes["rel"] == "alternate")
|
||||
item.link = attributes.getValueOrNull("href")
|
||||
}
|
||||
|
||||
private fun validateItem(item: Item) {
|
||||
when {
|
||||
item.title == null -> throw ParseException("Item title is required")
|
||||
item.link == null -> throw ParseException("Item link is required")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "id", "updated", "link", "author", "summary", "content")
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
package com.readrops.api.localfeed.atom
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.utils.*
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
import com.readrops.api.utils.extensions.nullableText
|
||||
import com.readrops.api.utils.extensions.nullableTextRecursively
|
||||
import com.readrops.db.entities.Item
|
||||
import org.joda.time.LocalDateTime
|
||||
import java.io.InputStream
|
||||
|
||||
class ATOMItemsAdapter : XmlAdapter<List<Item>> {
|
||||
|
||||
override fun fromXml(inputStream: InputStream): List<Item> {
|
||||
val konsumer = inputStream.konsumeXml()
|
||||
val items = arrayListOf<Item>()
|
||||
|
||||
return try {
|
||||
konsumer.child("feed") {
|
||||
allChildrenAutoIgnore("entry") {
|
||||
val item = Item().apply {
|
||||
allChildrenAutoIgnore(names) {
|
||||
when (tagName) {
|
||||
"title" -> title = nonNullText()
|
||||
"id" -> guid = nullableText()
|
||||
"updated" -> pubDate = DateUtils.parse(nullableText())
|
||||
"link" -> parseLink(this, this@apply)
|
||||
"author" -> allChildrenAutoIgnore("name") { author = nullableText() }
|
||||
"summary" -> description = nullableTextRecursively()
|
||||
"content" -> content = nullableTextRecursively()
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateItem(item)
|
||||
if (item.pubDate == null) item.pubDate = LocalDateTime.now()
|
||||
if (item.guid == null) item.guid = item.link
|
||||
|
||||
items += item
|
||||
}
|
||||
}
|
||||
|
||||
konsumer.close()
|
||||
items
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLink(konsumer: Konsumer, item: Item) {
|
||||
konsumer.apply {
|
||||
if (attributes.getValueOpt("rel") == null ||
|
||||
attributes["rel"] == "alternate")
|
||||
item.link = attributes.getValueOpt("href")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun validateItem(item: Item) {
|
||||
when {
|
||||
item.title == null -> throw ParseException("Item title is required")
|
||||
item.link == null -> throw ParseException("Item link is required")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "id", "updated", "link", "author", "summary", "content")
|
||||
}
|
||||
}
|
|
@ -4,18 +4,21 @@ import com.readrops.api.utils.exceptions.ParseException
|
|||
import com.readrops.api.utils.extensions.nextNonEmptyString
|
||||
import com.readrops.api.utils.extensions.nextNullableString
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.readrops.db.entities.Item
|
||||
import com.squareup.moshi.*
|
||||
|
||||
class JSONFeedAdapter {
|
||||
class JSONFeedAdapter : JsonAdapter<Pair<Feed, List<Item>>>() {
|
||||
|
||||
@ToJson
|
||||
fun toJson(feed: Feed) = ""
|
||||
override fun toJson(writer: JsonWriter, value: Pair<Feed, List<Item>>?) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@FromJson
|
||||
fun fromJson(reader: JsonReader): Feed = try {
|
||||
override fun fromJson(reader: JsonReader): Pair<Feed, List<Item>> = try {
|
||||
val feed = Feed()
|
||||
val items = arrayListOf<Item>()
|
||||
|
||||
val itemAdapter = JSONItemsAdapter()
|
||||
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
|
@ -25,19 +28,20 @@ class JSONFeedAdapter {
|
|||
1 -> siteUrl = reader.nextNullableString()
|
||||
2 -> url = reader.nextNullableString()
|
||||
3 -> description = reader.nextNullableString()
|
||||
4 -> items += itemAdapter.fromJson(reader)
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader.endObject()
|
||||
feed
|
||||
Pair(feed, items)
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names: JsonReader.Options = JsonReader.Options.of("title", "home_page_url",
|
||||
"feed_url", "description")
|
||||
"feed_url", "description", "items")
|
||||
}
|
||||
}
|
|
@ -17,59 +17,50 @@ class JSONItemsAdapter : JsonAdapter<List<Item>>() {
|
|||
// not useful
|
||||
}
|
||||
|
||||
override fun fromJson(reader: JsonReader): List<Item> = try {
|
||||
override fun fromJson(reader: JsonReader): List<Item> = with(reader) {
|
||||
val items = arrayListOf<Item>()
|
||||
reader.beginObject()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"items" -> parseItems(reader, items)
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
|
||||
private fun parseItems(reader: JsonReader, items: MutableList<Item>) = with(reader) {
|
||||
beginArray()
|
||||
|
||||
while (hasNext()) {
|
||||
beginObject()
|
||||
val item = Item()
|
||||
|
||||
var contentText: String? = null
|
||||
var contentHtml: String? = null
|
||||
try {
|
||||
beginArray()
|
||||
|
||||
while (hasNext()) {
|
||||
with(item) {
|
||||
when (selectName(names)) {
|
||||
0 -> guid = nextNonEmptyString()
|
||||
1 -> link = nextNonEmptyString()
|
||||
2 -> title = nextNonEmptyString()
|
||||
3 -> contentHtml = nextNullableString()
|
||||
4 -> contentText = nextNullableString()
|
||||
5 -> description = nextNullableString()
|
||||
6 -> imageLink = nextNullableString()
|
||||
7 -> pubDate = DateUtils.parse(nextNullableString())
|
||||
8 -> author = parseAuthor(reader) // jsonfeed 1.0
|
||||
9 -> author = parseAuthors(reader) // jsonfeed 1.1
|
||||
else -> skipValue()
|
||||
beginObject()
|
||||
val item = Item()
|
||||
|
||||
var contentText: String? = null
|
||||
var contentHtml: String? = null
|
||||
|
||||
while (hasNext()) {
|
||||
with(item) {
|
||||
when (selectName(names)) {
|
||||
0 -> guid = nextNonEmptyString()
|
||||
1 -> link = nextNonEmptyString()
|
||||
2 -> title = nextNonEmptyString()
|
||||
3 -> contentHtml = nextNullableString()
|
||||
4 -> contentText = nextNullableString()
|
||||
5 -> description = nextNullableString()
|
||||
6 -> imageLink = nextNullableString()
|
||||
7 -> pubDate = DateUtils.parse(nextNullableString())
|
||||
8 -> author = parseAuthor(reader) // jsonfeed 1.0
|
||||
9 -> author = parseAuthors(reader) // jsonfeed 1.1
|
||||
else -> skipValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateItem(item)
|
||||
item.content = if (contentHtml != null) contentHtml else contentText
|
||||
if (item.pubDate == null) item.pubDate = LocalDateTime.now()
|
||||
|
||||
endObject()
|
||||
items += item
|
||||
}
|
||||
|
||||
validateItem(item)
|
||||
item.content = if (contentHtml != null) contentHtml else contentText
|
||||
if (item.pubDate == null) item.pubDate = LocalDateTime.now()
|
||||
|
||||
endObject()
|
||||
items += item
|
||||
endArray()
|
||||
items
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
|
||||
endArray()
|
||||
}
|
||||
|
||||
private fun parseAuthor(reader: JsonReader): String? {
|
||||
|
|
|
@ -1,47 +1,58 @@
|
|||
package com.readrops.api.localfeed.rss1
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.localfeed.LocalRSSHelper
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.checkElement
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
import com.readrops.api.utils.extensions.nullableText
|
||||
import com.readrops.db.entities.Feed
|
||||
import java.io.InputStream
|
||||
import com.readrops.db.entities.Item
|
||||
|
||||
class RSS1FeedAdapter : XmlAdapter<Feed> {
|
||||
class RSS1FeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
|
||||
|
||||
override fun fromXml(inputStream: InputStream): Feed {
|
||||
val konsume = inputStream.konsumeXml()
|
||||
override fun fromXml(konsumer: Konsumer): Pair<Feed, List<Item>> {
|
||||
val feed = Feed()
|
||||
|
||||
return try {
|
||||
konsume.child("RDF") {
|
||||
allChildrenAutoIgnore("channel") {
|
||||
feed.url = attributes.getValueOpt("about",
|
||||
namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
|
||||
val items = arrayListOf<Item>()
|
||||
val itemAdapter = RSS1ItemAdapter()
|
||||
|
||||
allChildrenAutoIgnore(names) {
|
||||
with(feed) {
|
||||
when (tagName) {
|
||||
"title" -> name = nonNullText()
|
||||
"link" -> siteUrl = nonNullText()
|
||||
"description" -> description = nullableText()
|
||||
}
|
||||
}
|
||||
return try {
|
||||
konsumer.checkElement(LocalRSSHelper.RSS_1_ROOT_NAME) {
|
||||
it.allChildrenAutoIgnore(Names.of("channel", "item")) {
|
||||
when (tagName) {
|
||||
"channel" -> parseChannel(this, feed)
|
||||
"item" -> items += itemAdapter.fromXml(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
konsume.close()
|
||||
feed
|
||||
konsumer.close()
|
||||
Pair(feed, items)
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun parseChannel(konsumer: Konsumer, feed: Feed) = with(konsumer) {
|
||||
feed.url = attributes.getValueOrNull("about",
|
||||
namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
|
||||
|
||||
allChildrenAutoIgnore(names) {
|
||||
with(feed) {
|
||||
when (tagName) {
|
||||
"title" -> name = nonNullText()
|
||||
"link" -> siteUrl = nonNullText()
|
||||
"description" -> description = nullableText()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "link", "description")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package com.readrops.api.localfeed.rss1
|
||||
|
||||
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.localfeed.XmlAdapter.Companion.AUTHORS_MAX
|
||||
import com.readrops.api.utils.*
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
import com.readrops.api.utils.extensions.nullableText
|
||||
import com.readrops.api.utils.extensions.nullableTextRecursively
|
||||
import com.readrops.db.entities.Item
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
class RSS1ItemAdapter : XmlAdapter<Item> {
|
||||
|
||||
override fun fromXml(konsumer: Konsumer): Item {
|
||||
val item= Item()
|
||||
|
||||
return try {
|
||||
val authors = arrayListOf<String?>()
|
||||
val about = konsumer.attributes.getValueOrNull("about",
|
||||
namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
|
||||
|
||||
item.apply {
|
||||
konsumer.allChildrenAutoIgnore(names) {
|
||||
when (tagName) {
|
||||
"title" -> title = nonNullText()
|
||||
"link" -> link = nullableText()
|
||||
"dc:date" -> pubDate = DateUtils.parse(nullableText())
|
||||
"dc:creator" -> authors += nullableText()
|
||||
"description" -> description = nullableTextRecursively()
|
||||
"content:encoded" -> content = nullableTextRecursively()
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.pubDate == null) item.pubDate = LocalDateTime.now()
|
||||
if (item.link == null) item.link = about
|
||||
?: throw ParseException("RSS1 link or about element is required")
|
||||
item.guid = item.link
|
||||
|
||||
if (authors.filterNotNull().isNotEmpty()) item.author = authors.filterNotNull()
|
||||
.joinToString(limit = AUTHORS_MAX)
|
||||
|
||||
validateItem(item)
|
||||
item
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateItem(item: Item) {
|
||||
if (item.title == null) throw ParseException("Item title is required")
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "description", "date", "link", "creator", "encoded")
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
package com.readrops.api.localfeed.rss1
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX
|
||||
import com.readrops.api.utils.*
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
import com.readrops.api.utils.extensions.nullableText
|
||||
import com.readrops.api.utils.extensions.nullableTextRecursively
|
||||
import com.readrops.db.entities.Item
|
||||
import org.joda.time.LocalDateTime
|
||||
import java.io.InputStream
|
||||
|
||||
class RSS1ItemsAdapter : XmlAdapter<List<Item>> {
|
||||
|
||||
override fun fromXml(inputStream: InputStream): List<Item> {
|
||||
val konsumer = inputStream.konsumeXml()
|
||||
val items = arrayListOf<Item>()
|
||||
|
||||
return try {
|
||||
konsumer.child("RDF") {
|
||||
allChildrenAutoIgnore("item") {
|
||||
val authors = arrayListOf<String?>()
|
||||
val about = attributes.getValueOpt("about",
|
||||
namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
|
||||
|
||||
val item = Item().apply {
|
||||
allChildrenAutoIgnore(names) {
|
||||
when (tagName) {
|
||||
"title" -> title = nonNullText()
|
||||
"link" -> link = nullableText()
|
||||
"dc:date" -> pubDate = DateUtils.parse(nullableText())
|
||||
"dc:creator" -> authors += nullableText()
|
||||
"description" -> description = nullableTextRecursively()
|
||||
"content:encoded" -> content = nullableTextRecursively()
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.pubDate == null) item.pubDate = LocalDateTime.now()
|
||||
if (item.link == null) item.link = about
|
||||
?: throw ParseException("RSS1 link or about element is required")
|
||||
item.guid = item.link
|
||||
|
||||
if (authors.filterNotNull().isNotEmpty()) item.author = authors.filterNotNull()
|
||||
.joinToString(limit = AUTHORS_MAX)
|
||||
|
||||
validateItem(item)
|
||||
|
||||
items += item
|
||||
}
|
||||
}
|
||||
|
||||
konsumer.close()
|
||||
items
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateItem(item: Item) {
|
||||
if (item.title == null) throw ParseException("Item title is required")
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "description", "date", "link", "creator", "encoded")
|
||||
}
|
||||
}
|
|
@ -1,25 +1,29 @@
|
|||
package com.readrops.api.localfeed.rss2
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.localfeed.LocalRSSHelper
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.checkElement
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
import com.readrops.api.utils.extensions.nullableText
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Item
|
||||
import org.jsoup.Jsoup
|
||||
import java.io.InputStream
|
||||
|
||||
class RSS2FeedAdapter : XmlAdapter<Feed> {
|
||||
class RSS2FeedAdapter : XmlAdapter<Pair<Feed, List<Item>>> {
|
||||
|
||||
override fun fromXml(inputStream: InputStream): Feed {
|
||||
val konsume = inputStream.konsumeXml()
|
||||
override fun fromXml(konsumer: Konsumer): Pair<Feed, List<Item>> {
|
||||
val feed = Feed()
|
||||
|
||||
val items = arrayListOf<Item>()
|
||||
val itemAdapter = RSS2ItemAdapter()
|
||||
|
||||
return try {
|
||||
konsume.child("rss") {
|
||||
child("channel") {
|
||||
konsumer.checkElement(LocalRSSHelper.RSS_2_ROOT_NAME) {
|
||||
it.child("channel") {
|
||||
allChildrenAutoIgnore(names) {
|
||||
with(feed) {
|
||||
when (tagName) {
|
||||
|
@ -27,9 +31,10 @@ class RSS2FeedAdapter : XmlAdapter<Feed> {
|
|||
"description" -> description = nullableText()
|
||||
"link" -> siteUrl = nullableText()
|
||||
"atom:link" -> {
|
||||
if (attributes.getValueOpt("rel") == "self")
|
||||
url = attributes.getValueOpt("href")
|
||||
if (attributes.getValueOrNull("rel") == "self")
|
||||
url = attributes.getValueOrNull("href")
|
||||
}
|
||||
"item" -> items += itemAdapter.fromXml(this@allChildrenAutoIgnore)
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
|
@ -37,14 +42,14 @@ class RSS2FeedAdapter : XmlAdapter<Feed> {
|
|||
}
|
||||
}
|
||||
|
||||
konsume.close()
|
||||
feed
|
||||
konsumer.close()
|
||||
Pair(feed, items)
|
||||
} catch (e: Exception) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "description", "link")
|
||||
val names = Names.of("title", "description", "link", "item")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package com.readrops.api.localfeed.rss2
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.*
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX
|
||||
import com.readrops.api.utils.*
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
import com.readrops.api.utils.extensions.nullableText
|
||||
import com.readrops.api.utils.extensions.nullableTextRecursively
|
||||
import com.readrops.db.entities.Item
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
class RSS2ItemAdapter : XmlAdapter<Item> {
|
||||
|
||||
override fun fromXml(konsumer: Konsumer): Item {
|
||||
val item = Item()
|
||||
|
||||
return try {
|
||||
//konsumer.checkCurrent("item")
|
||||
val creators = arrayListOf<String?>()
|
||||
|
||||
item.apply {
|
||||
konsumer.allChildrenAutoIgnore(names) {
|
||||
when (tagName) {
|
||||
"title" -> title = ApiUtils.cleanText(nonNullText())
|
||||
"link" -> link = nonNullText()
|
||||
"author" -> author = nullableText()
|
||||
"dc:creator" -> creators += nullableText()
|
||||
"pubDate" -> pubDate = DateUtils.parse(nullableText())
|
||||
"dc:date" -> pubDate = DateUtils.parse(nullableText())
|
||||
"guid" -> guid = nullableText()
|
||||
"description" -> description = nullableTextRecursively()
|
||||
"content:encoded" -> content = nullableTextRecursively()
|
||||
"enclosure" -> parseEnclosure(this, item = this@apply)
|
||||
"media:content" -> parseMediaContent(this, item = this@apply)
|
||||
"media:group" -> parseMediaGroup(this, item = this@apply)
|
||||
else -> skipContents() // for example media:description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalizeItem(item, creators)
|
||||
item
|
||||
} catch (e: KonsumerException) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseEnclosure(konsumer: Konsumer, item: Item) = with(konsumer) {
|
||||
if (attributes.getValueOrNull("type") != null
|
||||
&& ApiUtils.isMimeImage(attributes["type"]) && item.imageLink == null)
|
||||
item.imageLink = attributes.getValueOrNull("url")
|
||||
}
|
||||
|
||||
private fun isMediumImage(konsumer: Konsumer) = with(konsumer) {
|
||||
attributes.getValueOrNull("medium") != null && ApiUtils.isMimeImage(attributes["medium"])
|
||||
}
|
||||
|
||||
private fun isTypeImage(konsumer: Konsumer) = with(konsumer) {
|
||||
attributes.getValueOrNull("type") != null && ApiUtils.isMimeImage(attributes["type"])
|
||||
}
|
||||
|
||||
private fun parseMediaContent(konsumer: Konsumer, item: Item) = with(konsumer) {
|
||||
if ((isMediumImage(konsumer) || isTypeImage(konsumer)) && item.imageLink == null)
|
||||
item.imageLink = konsumer.attributes.getValueOrNull("url")
|
||||
|
||||
konsumer.skipContents() // ignore media content sub elements
|
||||
}
|
||||
|
||||
private fun parseMediaGroup(konsumer: Konsumer, item: Item) = with(konsumer) {
|
||||
allChildrenAutoIgnore("content") {
|
||||
when (tagName) {
|
||||
"media:content" -> parseMediaContent(this, item)
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finalizeItem(item: Item, creators: List<String?>) = with(item) {
|
||||
validateItem(this)
|
||||
|
||||
if (pubDate == null) pubDate = LocalDateTime.now()
|
||||
if (guid == null) guid = link
|
||||
if (author == null && creators.filterNotNull().isNotEmpty())
|
||||
author = creators.filterNotNull().joinToString(limit = AUTHORS_MAX)
|
||||
}
|
||||
|
||||
private fun validateItem(item: Item) {
|
||||
when {
|
||||
item.title == null -> throw ParseException("Item title is required")
|
||||
item.link == null -> throw ParseException("Item link is required")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "link", "author", "creator", "pubDate", "date",
|
||||
"guid", "description", "encoded", "enclosure", "content", "group")
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
package com.readrops.api.localfeed.rss2
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.*
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX
|
||||
import com.readrops.api.utils.*
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
import com.readrops.api.utils.extensions.nullableText
|
||||
import com.readrops.api.utils.extensions.nullableTextRecursively
|
||||
import com.readrops.db.entities.Item
|
||||
import org.joda.time.LocalDateTime
|
||||
import java.io.InputStream
|
||||
|
||||
class RSS2ItemsAdapter : XmlAdapter<List<Item>> {
|
||||
|
||||
override fun fromXml(inputStream: InputStream): List<Item> {
|
||||
val konsumer = inputStream.konsumeXml()
|
||||
val items = mutableListOf<Item>()
|
||||
|
||||
return try {
|
||||
konsumer.child("rss") {
|
||||
child("channel") {
|
||||
allChildrenAutoIgnore("item") {
|
||||
val creators = arrayListOf<String?>()
|
||||
|
||||
val item = Item().apply {
|
||||
allChildrenAutoIgnore(names) {
|
||||
when (tagName) {
|
||||
"title" -> title = ApiUtils.cleanText(nonNullText())
|
||||
"link" -> link = nonNullText()
|
||||
"author" -> author = nullableText()
|
||||
"dc:creator" -> creators += nullableText()
|
||||
"pubDate" -> pubDate = DateUtils.parse(nullableText())
|
||||
"dc:date" -> pubDate = DateUtils.parse(nullableText())
|
||||
"guid" -> guid = nullableText()
|
||||
"description" -> description = nullableTextRecursively()
|
||||
"content:encoded" -> content = nullableTextRecursively()
|
||||
"enclosure" -> parseEnclosure(this, item = this@apply)
|
||||
"media:content" -> parseMediaContent(this, item = this@apply)
|
||||
"media:group" -> parseMediaGroup(this, item = this@apply)
|
||||
else -> skipContents() // for example media:description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalizeItem(item, creators)
|
||||
|
||||
items += item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
konsumer.close()
|
||||
items
|
||||
} catch (e: KonsumerException) {
|
||||
throw ParseException(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseEnclosure(konsumer: Konsumer, item: Item) = with(konsumer) {
|
||||
if (attributes.getValueOpt("type") != null
|
||||
&& ApiUtils.isMimeImage(attributes["type"]) && item.imageLink == null)
|
||||
item.imageLink = attributes.getValueOpt("url")
|
||||
}
|
||||
|
||||
private fun isMediumImage(konsumer: Konsumer) = with(konsumer) {
|
||||
attributes.getValueOpt("medium") != null && ApiUtils.isMimeImage(attributes["medium"])
|
||||
}
|
||||
|
||||
private fun isTypeImage(konsumer: Konsumer) = with(konsumer) {
|
||||
attributes.getValueOpt("type") != null && ApiUtils.isMimeImage(attributes["type"])
|
||||
}
|
||||
|
||||
private fun parseMediaContent(konsumer: Konsumer, item: Item) {
|
||||
if ((isMediumImage(konsumer) || isTypeImage(konsumer)) && item.imageLink == null)
|
||||
item.imageLink = konsumer.attributes.getValueOpt("url")
|
||||
|
||||
konsumer.skipContents() // ignore media content sub elements
|
||||
}
|
||||
|
||||
private fun parseMediaGroup(konsumer: Konsumer, item: Item) {
|
||||
konsumer.allChildrenAutoIgnore("content") {
|
||||
when (tagName) {
|
||||
"media:content" -> parseMediaContent(this, item)
|
||||
else -> skipContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finalizeItem(item: Item, creators: List<String?>) = with(item) {
|
||||
validateItem(this)
|
||||
|
||||
if (pubDate == null) pubDate = LocalDateTime.now()
|
||||
if (guid == null) guid = link
|
||||
if (author == null && creators.filterNotNull().isNotEmpty())
|
||||
author = creators.filterNotNull().joinToString(limit = AUTHORS_MAX)
|
||||
}
|
||||
|
||||
private fun validateItem(item: Item) {
|
||||
when {
|
||||
item.title == null -> throw ParseException("Item title is required")
|
||||
item.link == null -> throw ParseException("Item link is required")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val names = Names.of("title", "link", "author", "creator", "pubDate", "date",
|
||||
"guid", "description", "encoded", "enclosure", "content", "group")
|
||||
}
|
||||
}
|
|
@ -11,9 +11,11 @@ import com.readrops.api.services.nextcloudnews.adapters.NextNewsUserAdapter;
|
|||
import com.readrops.api.utils.ApiUtils;
|
||||
import com.readrops.api.utils.exceptions.ConflictException;
|
||||
import com.readrops.api.utils.exceptions.UnknownFormatException;
|
||||
import com.readrops.api.utils.extensions.KonsumerExtensionsKt;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.pojo.StarItem;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -23,7 +25,8 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.ResponseBody;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class NextNewsDataSource {
|
||||
|
@ -40,14 +43,16 @@ public class NextNewsDataSource {
|
|||
}
|
||||
|
||||
@Nullable
|
||||
public String login(String user) throws IOException {
|
||||
Response<ResponseBody> response = api.getUser(user).execute();
|
||||
public String login(OkHttpClient client, Account account) throws IOException {
|
||||
Request request = new Request.Builder()
|
||||
.url(account.getUrl() + "/ocs/v1.php/cloud/users/" + account.getLogin())
|
||||
.addHeader("OCS-APIRequest", "true")
|
||||
.build();
|
||||
|
||||
if (!response.isSuccessful()) {
|
||||
return null;
|
||||
}
|
||||
okhttp3.Response response = client.newCall(request).execute();
|
||||
|
||||
String displayName = new NextNewsUserAdapter().fromXml(response.body().byteStream());
|
||||
String displayName = new NextNewsUserAdapter().fromXml(KonsumerExtensionsKt
|
||||
.instantiateKonsumer(response.body().byteStream()));
|
||||
response.body().close();
|
||||
|
||||
return displayName;
|
||||
|
@ -284,7 +289,7 @@ public class NextNewsDataSource {
|
|||
|
||||
private int value;
|
||||
|
||||
ItemQueryType(int value) {
|
||||
ItemQueryType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
package com.readrops.api.services.nextcloudnews.adapters
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.localfeed.XmlAdapter
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.api.utils.extensions.nonNullText
|
||||
import java.io.InputStream
|
||||
|
||||
class NextNewsUserAdapter : XmlAdapter<String> {
|
||||
|
||||
override fun fromXml(inputStream: InputStream): String {
|
||||
val konsumer = inputStream.konsumeXml()
|
||||
override fun fromXml(konsumer: Konsumer): String {
|
||||
var displayName: String? = null
|
||||
|
||||
return try {
|
||||
|
|
|
@ -2,8 +2,11 @@ package com.readrops.api.utils.extensions
|
|||
|
||||
import com.gitlab.mvysny.konsumexml.Konsumer
|
||||
import com.gitlab.mvysny.konsumexml.Whitespace
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.gitlab.mvysny.konsumexml.textRecursively
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import java.io.InputStream
|
||||
import java.util.stream.Stream
|
||||
|
||||
fun Konsumer.nonNullText(): String {
|
||||
val text = text(whitespace = Whitespace.preserve)
|
||||
|
@ -18,4 +21,25 @@ fun Konsumer.nullableText(): String? {
|
|||
fun Konsumer.nullableTextRecursively(): String? {
|
||||
val text = textRecursively()
|
||||
return if (text.isNotEmpty()) text.trim() else null
|
||||
}
|
||||
}
|
||||
|
||||
fun Konsumer.checkRoot(name: String): Boolean = try {
|
||||
checkCurrent(name)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check is the element [name] is already loaded, loads it if it not the case and calls [block]
|
||||
*/
|
||||
fun Konsumer.checkElement(name: String, block: (Konsumer) -> Unit) {
|
||||
if (checkRoot(name)) block(this)
|
||||
else {
|
||||
child(name) {
|
||||
block(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun instantiateKonsumer(stream: InputStream) = stream.konsumeXml()
|
|
@ -59,7 +59,7 @@ class LocalRSSDataSourceTest : KoinTest {
|
|||
|
||||
@Test
|
||||
fun successfulQueryTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss_feed.xml")
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_feed.xml")
|
||||
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/xml; charset=UTF-8")
|
||||
|
@ -83,7 +83,7 @@ class LocalRSSDataSourceTest : KoinTest {
|
|||
|
||||
@Test
|
||||
fun headersTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss_feed.xml")
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_feed.xml")
|
||||
|
||||
mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
|
||||
.addHeader("Content-Type", "application/rss+xml; charset=UTF-8")
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.readrops.api.localfeed
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.Names
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import junit.framework.TestCase.*
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
|
@ -31,49 +33,46 @@ class LocalRSSHelperTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun rss1ContentTest() {
|
||||
assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream(
|
||||
"""<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/rss1full.xsl"?>
|
||||
<?xml-stylesheet type="text/css" media="screen" href="http://rss.slashdot.org/~d/styles/itemcontent.css"?>
|
||||
<rdf:RDF xmlns:admin="http://webns.net/mvcb/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://purl.org/rss/1.0/"
|
||||
""".trimIndent().toByteArray()
|
||||
)), LocalRSSHelper.RSSType.RSS_1)
|
||||
}
|
||||
fun guessRSSTypeRSS1Test() {
|
||||
val xml = """
|
||||
<RDF>
|
||||
<title></title>
|
||||
<description></description>
|
||||
</RDF>
|
||||
""".trimIndent()
|
||||
|
||||
val konsumer = xml.konsumeXml().nextElement(Names.of("RDF"))!!
|
||||
|
||||
@Test
|
||||
fun rss2ContentTest() {
|
||||
assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream(
|
||||
"""<rss
|
||||
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
version="2.0"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
|
||||
</rss>""".toByteArray()
|
||||
)), LocalRSSHelper.RSSType.RSS_2)
|
||||
assertEquals(LocalRSSHelper.guessRSSType(konsumer), LocalRSSHelper.RSSType.RSS_1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun atomContentTest() {
|
||||
assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream(
|
||||
"""<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
|
||||
</feed>""".toByteArray()
|
||||
)), LocalRSSHelper.RSSType.ATOM)
|
||||
fun guessRSSTypeATOMTest() {
|
||||
val xml = """
|
||||
<feed>
|
||||
<title></title>
|
||||
<description></description>
|
||||
</feed>
|
||||
""".trimIndent()
|
||||
|
||||
val konsumer = xml.konsumeXml().nextElement(Names.of("feed"))!!
|
||||
|
||||
assertEquals(LocalRSSHelper.guessRSSType(konsumer), LocalRSSHelper.RSSType.ATOM)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unknownContentTest() {
|
||||
assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream(
|
||||
"""<html>
|
||||
<body>
|
||||
</body>
|
||||
</html>""".trimMargin().toByteArray()
|
||||
)), LocalRSSHelper.RSSType.UNKNOWN)
|
||||
fun guessRSSTypeRSS2Test() {
|
||||
val xml = """
|
||||
<rss>
|
||||
<title></title>
|
||||
<description></description>
|
||||
</rss>
|
||||
""".trimIndent()
|
||||
|
||||
val konsumer = xml.konsumeXml().nextElement(Names.of("rss"))!!
|
||||
|
||||
assertEquals(LocalRSSHelper.guessRSSType(konsumer), LocalRSSHelper.RSSType.RSS_2)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
package com.readrops.api.localfeed
|
||||
|
||||
import com.readrops.api.localfeed.atom.ATOMFeedAdapter
|
||||
import com.readrops.api.localfeed.atom.ATOMItemsAdapter
|
||||
import com.readrops.api.localfeed.rss1.RSS1FeedAdapter
|
||||
import com.readrops.api.localfeed.rss1.RSS1ItemsAdapter
|
||||
import com.readrops.api.localfeed.rss2.RSS2FeedAdapter
|
||||
import com.readrops.api.localfeed.rss2.RSS2ItemsAdapter
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.ExpectedException
|
||||
|
@ -22,17 +20,8 @@ class XmlAdapterTest {
|
|||
assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSS2FeedAdapter)
|
||||
assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMFeedAdapter)
|
||||
|
||||
expectedException.expect(IllegalArgumentException::class.java)
|
||||
XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun xmlItemsAdapterFactoryTest() {
|
||||
assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.RSS_1) is RSS1ItemsAdapter)
|
||||
assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSS2ItemsAdapter)
|
||||
assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMItemsAdapter)
|
||||
|
||||
expectedException.expect(IllegalArgumentException::class.java)
|
||||
XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN)
|
||||
assertThrows(java.lang.IllegalArgumentException::class.java) {
|
||||
XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package com.readrops.api.localfeed.atom
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.lang.Exception
|
||||
|
||||
class ATOMAdapterTest {
|
||||
|
||||
private val adapter = ATOMFeedAdapter()
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/atom/atom_items.xml")
|
||||
|
||||
val pair = adapter.fromXml(stream.konsumeXml())
|
||||
val feed = pair.first
|
||||
val items = pair.second
|
||||
|
||||
with(feed) {
|
||||
assertEquals(name, "Recent Commits to Readrops:develop")
|
||||
assertEquals(url, "https://github.com/readrops/Readrops/commits/develop.atom")
|
||||
assertEquals(siteUrl, "https://github.com/readrops/Readrops/commits/develop")
|
||||
assertEquals(description, "Here is a subtitle")
|
||||
}
|
||||
|
||||
with(items[0]) {
|
||||
assertEquals(items.size, 4)
|
||||
assertEquals(title, "Add an option to open item url in custom tab")
|
||||
assertEquals(link, "https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac")
|
||||
assertEquals(pubDate, DateUtils.parse("2020-09-06T21:09:59Z"))
|
||||
assertEquals(author, "Shinokuni")
|
||||
assertEquals(description, "Summary")
|
||||
assertEquals(guid, "tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac")
|
||||
TestCase.assertNotNull(content)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noDateTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/atom/atom_items_no_date.xml")
|
||||
|
||||
val item = adapter.fromXml(stream.konsumeXml()).second[0]
|
||||
TestCase.assertNotNull(item.pubDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noTitleTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/atom/atom_items_no_title.xml")
|
||||
|
||||
val exception = assertThrows(ParseException::class.java) {
|
||||
adapter.fromXml(stream.konsumeXml())
|
||||
}
|
||||
|
||||
assertTrue(exception.message!!.contains("Item title is required"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noLinkTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/atom/atom_items_no_link.xml")
|
||||
|
||||
val exception = assertThrows(ParseException::class.java) {
|
||||
adapter.fromXml(stream.konsumeXml())
|
||||
}
|
||||
|
||||
assertTrue(exception.message!!.contains("Item link is required"))
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package com.readrops.api.localfeed.atom
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ATOMFeedAdapterTest {
|
||||
|
||||
private val adapter = ATOMFeedAdapter()
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/atom/atom_feed.xml")
|
||||
|
||||
val feed = adapter.fromXml(stream)
|
||||
|
||||
assertEquals(feed.name, "Recent Commits to Readrops:develop")
|
||||
assertEquals(feed.url, "https://github.com/readrops/Readrops/commits/develop.atom")
|
||||
assertEquals(feed.siteUrl, "https://github.com/readrops/Readrops/commits/develop")
|
||||
assertEquals(feed.description, "Here is a subtitle")
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package com.readrops.api.localfeed.atom
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.ExpectedException
|
||||
|
||||
class ATOMItemsAdapterTest {
|
||||
|
||||
private val adapter = ATOMItemsAdapter()
|
||||
|
||||
@get:Rule
|
||||
val expectedException: ExpectedException = ExpectedException.none()
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/atom/atom_items.xml")
|
||||
|
||||
val items = adapter.fromXml(stream)
|
||||
val item = items[0]
|
||||
|
||||
assertEquals(items.size, 4)
|
||||
assertEquals(item.title, "Add an option to open item url in custom tab")
|
||||
assertEquals(item.link, "https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac")
|
||||
assertEquals(item.pubDate, DateUtils.parse("2020-09-06T21:09:59Z"))
|
||||
assertEquals(item.author, "Shinokuni")
|
||||
assertEquals(item.description, "Summary")
|
||||
assertEquals(item.guid, "tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac")
|
||||
assertNotNull(item.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noDateTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/atom/atom_items_no_date.xml")
|
||||
|
||||
val item = adapter.fromXml(stream).first()
|
||||
assertNotNull(item.pubDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noTitleTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/atom/atom_items_no_title.xml")
|
||||
|
||||
expectedException.expect(ParseException::class.java)
|
||||
expectedException.expectMessage("Item title is required")
|
||||
|
||||
adapter.fromXml(stream)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noLinkTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/atom/atom_items_no_link.xml")
|
||||
|
||||
expectedException.expect(ParseException::class.java)
|
||||
expectedException.expectMessage("Item link is required")
|
||||
|
||||
adapter.fromXml(stream)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,29 +1,95 @@
|
|||
package com.readrops.api.localfeed.json
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Item
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import okio.Buffer
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
|
||||
class JSONFeedAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(JSONFeedAdapter())
|
||||
.add(Types.newParameterizedType(Pair::class.java, Feed::class.java,
|
||||
Types.newParameterizedType(List::class.java, Item::class.java)), JSONFeedAdapter())
|
||||
.build()
|
||||
.adapter(Feed::class.java)
|
||||
.adapter<Pair<Feed, List<Item>>>(Types.newParameterizedType(Pair::class.java, Feed::class.java,
|
||||
Types.newParameterizedType(List::class.java, Item::class.java)))
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/json/json_feed.json")
|
||||
|
||||
val feed = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
val pair = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
val feed = pair.first
|
||||
val items = pair.second
|
||||
|
||||
assertEquals(feed.name, "News from Flying Meat")
|
||||
assertEquals(feed.url, "http://flyingmeat.com/blog/feed.json")
|
||||
assertEquals(feed.siteUrl, "http://flyingmeat.com/blog/")
|
||||
assertEquals(feed.description, "News from your friends at Flying Meat.")
|
||||
with(feed) {
|
||||
assertEquals(name, "News from Flying Meat")
|
||||
assertEquals(url, "http://flyingmeat.com/blog/feed.json")
|
||||
assertEquals(siteUrl, "http://flyingmeat.com/blog/")
|
||||
assertEquals(description, "News from your friends at Flying Meat.")
|
||||
}
|
||||
|
||||
with(items[0]) {
|
||||
assertEquals(items.size, 10)
|
||||
assertEquals(guid, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html")
|
||||
assertEquals(title, "Acorn and 10.13")
|
||||
assertEquals(link, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html")
|
||||
assertEquals(pubDate, DateUtils.parse("2017-09-25T14:27:27-07:00"))
|
||||
assertEquals(author, "Author 1")
|
||||
TestCase.assertNotNull(content)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun otherCasesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/json/json_items_other_cases.json")
|
||||
|
||||
val item = adapter.fromJson(Buffer().readFrom(stream))!!.second[0]
|
||||
|
||||
assertEquals(item.description, "This is a summary")
|
||||
assertEquals(item.content, "content_html")
|
||||
assertEquals(item.imageLink, "https://image.com")
|
||||
assertEquals(item.author, "Author 1, Author 3, Author 4, Author 5, ...")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullDateTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/json/json_items_no_date.json")
|
||||
|
||||
val item = adapter.fromJson(Buffer().readFrom(stream))!!.second[0]
|
||||
TestCase.assertNotNull(item.pubDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullTitleTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/json/json_items_no_title.json")
|
||||
|
||||
val exception = assertThrows(ParseException::class.java) {
|
||||
adapter.fromJson(Buffer().readFrom(stream))
|
||||
}
|
||||
|
||||
assertEquals("Item title is required", exception.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullLinkTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/json/json_items_no_link.json")
|
||||
|
||||
val exception = assertThrows(ParseException::class.java) {
|
||||
adapter.fromJson(Buffer().readFrom(stream))
|
||||
}
|
||||
|
||||
assertEquals("Item link is required", exception.message)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
package com.readrops.api.localfeed.json
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import com.readrops.db.entities.Item
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import okio.Buffer
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.ExpectedException
|
||||
|
||||
|
||||
class JSONItemsAdapterTest {
|
||||
|
||||
private val adapter = Moshi.Builder()
|
||||
.add(Types.newParameterizedType(List::class.java, Item::class.java), JSONItemsAdapter())
|
||||
.build()
|
||||
.adapter<List<Item>>(Types.newParameterizedType(List::class.java, Item::class.java))
|
||||
|
||||
@get:Rule
|
||||
val expectedException: ExpectedException = ExpectedException.none()
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/json/json_feed.json")
|
||||
|
||||
val items = adapter.fromJson(Buffer().readFrom(stream))!!
|
||||
val item = items.first()
|
||||
|
||||
assertEquals(items.size, 10)
|
||||
assertEquals(item.guid, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html")
|
||||
assertEquals(item.title, "Acorn and 10.13")
|
||||
assertEquals(item.link, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html")
|
||||
assertEquals(item.pubDate, DateUtils.parse("2017-09-25T14:27:27-07:00"))
|
||||
assertEquals(item.author, "Author 1")
|
||||
assertNotNull(item.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun otherCasesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/json/json_items_other_cases.json")
|
||||
|
||||
val item = adapter.fromJson(Buffer().readFrom(stream))!!.first()
|
||||
|
||||
assertEquals(item.description, "This is a summary")
|
||||
assertEquals(item.content, "content_html")
|
||||
assertEquals(item.imageLink, "https://image.com")
|
||||
assertEquals(item.author, "Author 1, Author 3, Author 4, Author 5, ...")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullDateTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/json/json_items_no_date.json")
|
||||
|
||||
val item = adapter.fromJson(Buffer().readFrom(stream))!!.first()
|
||||
assertNotNull(item.pubDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullTitleTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/json/json_items_no_title.json")
|
||||
|
||||
expectedException.expect(ParseException::class.java)
|
||||
expectedException.expectMessage("Item title is required")
|
||||
|
||||
adapter.fromJson(Buffer().readFrom(stream))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullLinkTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/json/json_items_no_link.json")
|
||||
|
||||
expectedException.expect(ParseException::class.java)
|
||||
expectedException.expectMessage("Item link is required")
|
||||
|
||||
adapter.fromJson(Buffer().readFrom(stream))
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package com.readrops.api.localfeed.rss1
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import junit.framework.Assert.assertEquals
|
||||
import junit.framework.Assert.assertNotNull
|
||||
import junit.framework.TestCase
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class RSS1AdapterTest {
|
||||
|
||||
private val adapter = RSS1FeedAdapter()
|
||||
|
||||
@Test
|
||||
fun normalCaseTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss1/rss1_feed.xml")
|
||||
|
||||
val pair = adapter.fromXml(stream.konsumeXml())
|
||||
val feed = pair.first
|
||||
val items = pair.second
|
||||
|
||||
with(feed) {
|
||||
assertEquals(name, "Slashdot")
|
||||
assertEquals(url, "https://slashdot.org/")
|
||||
assertEquals(siteUrl, "https://slashdot.org/")
|
||||
assertEquals(description, "News for nerds, stuff that matters")
|
||||
}
|
||||
|
||||
with(items[0]) {
|
||||
assertEquals(items.size, 4)
|
||||
assertEquals(title, "Google Expands its Flutter Development Kit To Windows Apps")
|
||||
assertEquals(link!!.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" +
|
||||
"its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed")
|
||||
assertEquals(guid!!.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" +
|
||||
"its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed")
|
||||
assertEquals(pubDate, DateUtils.parse("2020-09-23T16:15:00+00:00"))
|
||||
assertEquals(author, "msmash")
|
||||
assertNotNull(description)
|
||||
assertEquals(content, "content:encoded")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun specialCasesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_special_cases.xml")
|
||||
|
||||
val item = adapter.fromXml(stream.konsumeXml()).second[0]
|
||||
|
||||
TestCase.assertEquals(item.author, "msmash, creator 2, creator 3, creator 4, ...")
|
||||
TestCase.assertEquals(item.link, "https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-" +
|
||||
"that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&utm_medium=feed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullDateTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_no_date.xml")
|
||||
|
||||
val item = adapter.fromXml(stream.konsumeXml()).second[0]
|
||||
TestCase.assertNotNull(item.pubDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullTitleTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_no_title.xml")
|
||||
|
||||
val exception = assertThrows(ParseException::class.java) {
|
||||
adapter.fromXml(stream.konsumeXml())
|
||||
}
|
||||
|
||||
assertTrue(exception.message!!.contains("Item title is required"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullLinkTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_no_link.xml")
|
||||
|
||||
val exception = assertThrows(ParseException::class.java) {
|
||||
adapter.fromXml(stream.konsumeXml())
|
||||
}
|
||||
|
||||
assertTrue(exception.message!!.contains("RSS1 link or about element is required"))
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package com.readrops.api.localfeed.rss1
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import junit.framework.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class RSS1FeedAdapterTest {
|
||||
|
||||
private val adapter = RSS1FeedAdapter()
|
||||
|
||||
@Test
|
||||
fun normalCaseTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss1/rss1_feed.xml")
|
||||
|
||||
val feed = adapter.fromXml(stream)
|
||||
|
||||
assertEquals(feed.name, "Slashdot")
|
||||
assertEquals(feed.url, "https://slashdot.org/")
|
||||
assertEquals(feed.siteUrl, "https://slashdot.org/")
|
||||
assertEquals(feed.description, "News for nerds, stuff that matters")
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package com.readrops.api.localfeed.rss1
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.ExpectedException
|
||||
|
||||
class RSS1ItemsAdapterTest {
|
||||
|
||||
private val adapter = RSS1ItemsAdapter()
|
||||
|
||||
@get:Rule
|
||||
val expectedException: ExpectedException = ExpectedException.none()
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss1/rss1_feed.xml")
|
||||
|
||||
val items = adapter.fromXml(stream)
|
||||
val item = items.first()
|
||||
|
||||
assertEquals(items.size, 4)
|
||||
assertEquals(item.title, "Google Expands its Flutter Development Kit To Windows Apps")
|
||||
assertEquals(item.link!!.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" +
|
||||
"its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed")
|
||||
assertEquals(item.guid!!.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" +
|
||||
"its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed")
|
||||
assertEquals(item.pubDate, DateUtils.parse("2020-09-23T16:15:00+00:00"))
|
||||
assertEquals(item.author, "msmash")
|
||||
assertNotNull(item.description)
|
||||
assertEquals(item.content, "content:encoded")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun specialCasesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_special_cases.xml")
|
||||
|
||||
val item = adapter.fromXml(stream).first()
|
||||
|
||||
assertEquals(item.author, "msmash, creator 2, creator 3, creator 4, ...")
|
||||
assertEquals(item.link, "https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-" +
|
||||
"that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&utm_medium=feed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullDateTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_no_date.xml")
|
||||
|
||||
val item = adapter.fromXml(stream).first()
|
||||
assertNotNull(item.pubDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullTitleTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_no_title.xml")
|
||||
|
||||
expectedException.expect(ParseException::class.java)
|
||||
expectedException.expectMessage("Item title is required")
|
||||
|
||||
adapter.fromXml(stream)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nullLinkTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_no_link.xml")
|
||||
|
||||
expectedException.expect(ParseException::class.java)
|
||||
expectedException.expectMessage("RSS1 link or about element is required")
|
||||
|
||||
adapter.fromXml(stream)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package com.readrops.api.localfeed.rss2
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import junit.framework.TestCase
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
|
||||
class RSS2AdapterTest {
|
||||
|
||||
private val adapter = RSS2FeedAdapter()
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_feed.xml")
|
||||
|
||||
val pair = adapter.fromXml(stream.konsumeXml())
|
||||
val feed = pair.first
|
||||
val items = pair.second
|
||||
|
||||
with(feed) {
|
||||
assertEquals(name, "Hacker News")
|
||||
assertEquals(url, "https://news.ycombinator.com/feed/")
|
||||
assertEquals(siteUrl, "https://news.ycombinator.com/")
|
||||
assertEquals(description, "Links for the intellectually curious, ranked by readers.")
|
||||
}
|
||||
|
||||
with(items[0]) {
|
||||
assertEquals(items.size, 7)
|
||||
assertEquals(title, "Africa declared free of wild polio")
|
||||
assertEquals(link, "https://www.bbc.com/news/world-africa-53887947")
|
||||
assertEquals(pubDate, DateUtils.parse("Tue, 25 Aug 2020 17:15:49 +0000"))
|
||||
assertEquals(author, "Author 1")
|
||||
assertEquals(description, "<a href=\"https://news.ycombinator.com/item?id=24273602\">Comments</a>")
|
||||
assertEquals(guid, "https://www.bbc.com/news/world-africa-53887947")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun nullTitleTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_feed_special_cases.xml")
|
||||
|
||||
assertThrows(ParseException::class.java) {
|
||||
adapter.fromXml(stream.konsumeXml())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun otherNamespacesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_other_namespaces.xml")
|
||||
val item = adapter.fromXml(stream.konsumeXml()).second[0]
|
||||
|
||||
assertEquals(item.guid, "guid")
|
||||
assertEquals(item.author, "creator 1, creator 2, creator 3, creator 4")
|
||||
assertEquals(item.pubDate, DateUtils.parse("2020-08-05T14:03:48Z"))
|
||||
assertEquals(item.content, "content:encoded")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noDateTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_no_date.xml")
|
||||
val item = adapter.fromXml(stream.konsumeXml()).second[0]
|
||||
|
||||
TestCase.assertNotNull(item.pubDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noTitleTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_no_title.xml")
|
||||
|
||||
val exception = assertThrows(ParseException::class.java) {
|
||||
adapter.fromXml(stream.konsumeXml())
|
||||
}
|
||||
|
||||
assertTrue(exception.message!!.contains("Item title is required"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noLinkTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_no_link.xml")
|
||||
|
||||
val exception = assertThrows(ParseException::class.java) {
|
||||
adapter.fromXml(stream.konsumeXml())
|
||||
}
|
||||
|
||||
assertTrue(exception.message!!.contains("Item link is required"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enclosureTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_enclosure.xml")
|
||||
val item = adapter.fromXml(stream.konsumeXml()).second[0]
|
||||
|
||||
assertEquals(item.imageLink, "https://image1.jpg")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediaContentTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_media_content.xml")
|
||||
val items = adapter.fromXml(stream.konsumeXml())
|
||||
|
||||
assertEquals(items.second[0].imageLink, "https://image1.jpg")
|
||||
assertEquals(items.second[1].imageLink, "https://image2.jpg")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediaGroupTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_media_group.xml")
|
||||
val item = adapter.fromXml(stream.konsumeXml()).second[0]
|
||||
|
||||
assertEquals(item.imageLink, "https://image1.jpg")
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package com.readrops.api.localfeed.rss2
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class RSS2FeedAdapterTest {
|
||||
|
||||
|
||||
private val adapter = RSS2FeedAdapter()
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_full_feed.xml")
|
||||
|
||||
val feed = adapter.fromXml(stream)
|
||||
|
||||
assertEquals(feed.name, "Hacker News")
|
||||
assertEquals(feed.url, "https://news.ycombinator.com/feed/")
|
||||
assertEquals(feed.siteUrl, "https://news.ycombinator.com/")
|
||||
assertEquals(feed.description, "Links for the intellectually curious, ranked by readers.")
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = ParseException::class)
|
||||
fun nullTitleTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_feed_special_cases.xml")
|
||||
adapter.fromXml(stream)
|
||||
}
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
package com.readrops.api.localfeed.rss2
|
||||
|
||||
import com.readrops.api.TestUtils
|
||||
import com.readrops.api.utils.DateUtils
|
||||
import com.readrops.api.utils.exceptions.ParseException
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.ExpectedException
|
||||
|
||||
class RSS2ItemsAdapterTest {
|
||||
|
||||
private val adapter = RSS2ItemsAdapter()
|
||||
|
||||
@get:Rule
|
||||
val expectedException: ExpectedException = ExpectedException.none()
|
||||
|
||||
@Test
|
||||
fun normalCasesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss_feed.xml")
|
||||
|
||||
val items = adapter.fromXml(stream)
|
||||
val item = items.first()
|
||||
|
||||
assertEquals(items.size, 7)
|
||||
assertEquals(item.title, "Africa declared free of wild polio")
|
||||
assertEquals(item.link, "https://www.bbc.com/news/world-africa-53887947")
|
||||
assertEquals(item.pubDate, DateUtils.parse("Tue, 25 Aug 2020 17:15:49 +0000"))
|
||||
assertEquals(item.author, "Author 1")
|
||||
assertEquals(item.description, "<a href=\"https://news.ycombinator.com/item?id=24273602\">Comments</a>")
|
||||
assertEquals(item.guid, "https://www.bbc.com/news/world-africa-53887947")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun otherNamespacesTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_other_namespaces.xml")
|
||||
val item = adapter.fromXml(stream).first()
|
||||
|
||||
assertEquals(item.guid, "guid")
|
||||
assertEquals(item.author, "creator 1, creator 2, creator 3, creator 4")
|
||||
assertEquals(item.pubDate, DateUtils.parse("2020-08-05T14:03:48Z"))
|
||||
assertEquals(item.content, "content:encoded")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noDateTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_no_date.xml")
|
||||
val item = adapter.fromXml(stream).first()
|
||||
|
||||
assertNotNull(item.pubDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noTitleTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_no_title.xml")
|
||||
|
||||
expectedException.expect(ParseException::class.java)
|
||||
expectedException.expectMessage("Item title is required")
|
||||
|
||||
adapter.fromXml(stream)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noLinkTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_no_link.xml")
|
||||
|
||||
expectedException.expect(ParseException::class.java)
|
||||
expectedException.expectMessage("Item link is required")
|
||||
|
||||
adapter.fromXml(stream)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enclosureTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_enclosure.xml")
|
||||
val item = adapter.fromXml(stream).first()
|
||||
|
||||
assertEquals(item.imageLink, "https://image1.jpg")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediaContentTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_media_content.xml")
|
||||
val items = adapter.fromXml(stream)
|
||||
|
||||
assertEquals(items.first().imageLink, "https://image1.jpg")
|
||||
assertEquals(items[1].imageLink, "https://image2.jpg")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediaGroupTest() {
|
||||
val stream = TestUtils.loadResource("localfeed/rss2/rss_items_media_group.xml")
|
||||
val item = adapter.fromXml(stream).first()
|
||||
|
||||
assertEquals(item.imageLink, "https://image1.jpg")
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.readrops.api.services.nextcloudnews.adapters
|
||||
|
||||
import com.gitlab.mvysny.konsumexml.konsumeXml
|
||||
import com.readrops.api.TestUtils
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
@ -12,6 +13,6 @@ class NextNewsUserAdapterTest {
|
|||
fun validXmlTest() {
|
||||
val stream = TestUtils.loadResource("services/nextcloudnews/user.xml")
|
||||
|
||||
assertEquals(adapter.fromXml(stream), "Shinokuni")
|
||||
assertEquals(adapter.fromXml(stream.konsumeXml()), "Shinokuni")
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
<link type="application/atom+xml" rel="self" href="https://github.com/readrops/Readrops/commits/develop.atom"/>
|
||||
<title>Recent Commits to Readrops:develop</title>
|
||||
<updated>2020-09-06T21:09:59Z</updated>
|
||||
<subtitle>Here is a subtitle</subtitle>
|
||||
<entry>
|
||||
<id>tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac</id>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac"/>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
|
||||
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/"
|
||||
xmlns:atom="http://www.w3.org/1999/xhtml">
|
||||
<channel>
|
||||
<title>Hacker News</title>
|
||||
<atom:link href="https://news.ycombinator.com/feed/" rel="self" />
|
||||
<link>https://news.ycombinator.com/</link>
|
||||
<description>Links for the intellectually curious, ranked by readers.</description>
|
||||
<item>
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
|
||||
<channel>
|
||||
<title>Hacker News</title>
|
||||
<atom:link href="https://news.ycombinator.com/feed/" rel="self" />
|
||||
<atom:link href="https://news.ycombinator.com/hub" rel="hub" />
|
||||
<link>https://news.ycombinator.com/</link>
|
||||
<description>Links for the intellectually curious, ranked by readers.</description>
|
||||
</channel>
|
||||
</rss>
|
|
@ -12,7 +12,6 @@ import com.readrops.api.services.SyncResult;
|
|||
import com.readrops.api.services.SyncType;
|
||||
import com.readrops.api.services.nextcloudnews.NextNewsDataSource;
|
||||
import com.readrops.api.services.nextcloudnews.NextNewsSyncData;
|
||||
import com.readrops.api.services.nextcloudnews.adapters.NextNewsUserAdapter;
|
||||
import com.readrops.api.utils.exceptions.UnknownFormatException;
|
||||
import com.readrops.app.addfeed.FeedInsertionResult;
|
||||
import com.readrops.app.addfeed.ParsingResult;
|
||||
|
@ -37,8 +36,6 @@ import io.reactivex.Completable;
|
|||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class NextNewsRepository extends ARepository {
|
||||
|
||||
|
@ -58,22 +55,8 @@ public class NextNewsRepository extends ARepository {
|
|||
return Single.<String>create(emitter -> {
|
||||
OkHttpClient httpClient = KoinJavaComponent.get(OkHttpClient.class);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(account.getUrl() + "/ocs/v1.php/cloud/users/" + account.getLogin())
|
||||
.addHeader("OCS-APIRequest", "true")
|
||||
.build();
|
||||
|
||||
Response response = httpClient.newCall(request).execute();
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
String displayName = new NextNewsUserAdapter().fromXml(response.body().byteStream());
|
||||
response.body().close();
|
||||
|
||||
emitter.onSuccess(displayName);
|
||||
} else {
|
||||
// TODO better error handling
|
||||
emitter.onError(new Exception("Login exception : " + response.code() + " error"));
|
||||
}
|
||||
String displayName = dataSource.login(httpClient, account);
|
||||
emitter.onSuccess(displayName);
|
||||
}).flatMapCompletable(displayName -> {
|
||||
account.setDisplayedName(displayName);
|
||||
account.setCurrentAccount(true);
|
||||
|
|
|
@ -14,5 +14,5 @@ data class ItemWithFeed(
|
|||
@ColumnInfo(name = "background_color") @ColorInt val bgColor: Int,
|
||||
@ColumnInfo(name = "icon_url") val feedIconUrl: String?,
|
||||
@ColumnInfo(name = "siteUrl") val websiteUrl: String?,
|
||||
@Embedded(prefix = "folder_") val folder: Folder,
|
||||
@Embedded(prefix = "folder_") val folder: Folder?,
|
||||
)
|
Loading…
Reference in New Issue