Merge branch 'feature/local_rss_memory_improvements' into develop

This commit is contained in:
Shinokuni 2021-09-05 14:10:14 +02:00
commit a3c3e7af1e
37 changed files with 881 additions and 963 deletions

View File

@ -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
}
}
}
}
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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? {

View File

@ -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")
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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()

View File

@ -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")

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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"))
}
}

View File

@ -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")
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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"))
}
}

View File

@ -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")
}
}

View File

@ -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)
}
}

View File

@ -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")
}
}

View File

@ -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)
}
}

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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"/>

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

@ -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?,
)