Readrops/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt
2021-07-02 18:56:11 +02:00

152 lines
6.0 KiB
Kotlin

package com.readrops.api.localfeed
import android.accounts.NetworkErrorException
import androidx.annotation.WorkerThread
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
import com.readrops.api.utils.exceptions.UnknownFormatException
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Item
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
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.net.HttpURLConnection
class LocalRSSDataSource(private val httpClient: OkHttpClient): KoinComponent {
/**
* Query RSS url
* @param url url to query
* @param headers request headers
* @return a Feed object with its items
*/
@Throws(ParseException::class, UnknownFormatException::class, NetworkErrorException::class, IOException::class)
@WorkerThread
fun queryRSSResource(url: String, headers: Headers?): Pair<Feed, List<Item>>? {
get<AuthInterceptor>().credentials = null
val response = queryUrl(url, headers)
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)
response.body?.close()
Pair(feed, items)
}
response.code == HttpURLConnection.HTTP_NOT_MODIFIED -> null
else -> throw NetworkErrorException("$url returned ${response.code} code : ${response.message}")
}
}
/**
* Checks if the provided url is a RSS resource
* @param url url to check
* @return true if [url] is a RSS resource, false otherwise
*/
@WorkerThread
fun isUrlRSSResource(url: String): Boolean {
val response = queryUrl(url, null)
return if (response.isSuccessful) {
val header = response.header(ApiUtils.CONTENT_TYPE_HEADER)
?: return false
val contentType = ApiUtils.parseContentType(header)
?: return false
var type = LocalRSSHelper.getRSSType(contentType)
if (type == LocalRSSHelper.RSSType.UNKNOWN)
type = LocalRSSHelper.getRSSContentType(response.body?.byteStream()!!) // stream is closed in helper method
type != LocalRSSHelper.RSSType.UNKNOWN
} else false
}
@Throws(IOException::class)
private fun queryUrl(url: String, headers: Headers?): Response {
val requestBuilder = Request.Builder().url(url)
headers?.let { requestBuilder.headers(it) }
return httpClient.newCall(requestBuilder.build()).execute()
}
private fun parseFeed(stream: InputStream, type: LocalRSSHelper.RSSType, response: Response): Feed {
val feed = if (type != LocalRSSHelper.RSSType.JSONFEED) {
val adapter = XmlAdapter.xmlFeedAdapterFactory(type)
adapter.fromXml(stream)
} else {
val adapter = Moshi.Builder()
.add(JSONFeedAdapter())
.build()
.adapter(Feed::class.java)
adapter.fromJson(Buffer().readFrom(stream))!!
}
handleSpecialCases(feed, type, response)
feed.etag = response.header(ApiUtils.ETAG_HEADER)
feed.lastModified = response.header(ApiUtils.LAST_MODIFIED_HEADER)
return feed
}
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
}
}
}
}