175 lines
5.7 KiB
Kotlin
175 lines
5.7 KiB
Kotlin
package com.rssuper.services
|
|
|
|
import com.rssuper.parsing.FeedParser
|
|
import com.rssuper.parsing.ParseResult
|
|
import okhttp3.Call
|
|
import okhttp3.EventListener
|
|
import okhttp3.OkHttpClient
|
|
import okhttp3.Request
|
|
import okhttp3.Response
|
|
import java.io.IOException
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
class FeedFetcher(
|
|
private val timeoutMs: Long = 15000,
|
|
private val maxRetries: Int = 3,
|
|
private val baseRetryDelayMs: Long = 1000
|
|
) {
|
|
private val client: OkHttpClient
|
|
|
|
init {
|
|
val builder = OkHttpClient.Builder()
|
|
.connectTimeout(timeoutMs, TimeUnit.MILLISECONDS)
|
|
.readTimeout(timeoutMs, TimeUnit.MILLISECONDS)
|
|
.writeTimeout(timeoutMs, TimeUnit.MILLISECONDS)
|
|
|
|
builder.eventListenerFactory { call -> TimeoutEventListener(call) }
|
|
|
|
client = builder.build()
|
|
}
|
|
|
|
fun fetch(
|
|
url: String,
|
|
httpAuth: HTTPAuthCredentials? = null,
|
|
ifNoneMatch: String? = null,
|
|
ifModifiedSince: String? = null
|
|
): NetworkResult<FetchResult> {
|
|
var lastError: Throwable? = null
|
|
|
|
for (attempt in 1..maxRetries) {
|
|
val result = fetchSingleAttempt(url, httpAuth, ifNoneMatch, ifModifiedSince)
|
|
|
|
when (result) {
|
|
is NetworkResult.Success -> return result
|
|
is NetworkResult.Failure -> {
|
|
lastError = result.error
|
|
if (attempt < maxRetries) {
|
|
val delay = calculateBackoffDelay(attempt)
|
|
Thread.sleep(delay)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return NetworkResult.Failure(lastError ?: NetworkError.Unknown())
|
|
}
|
|
|
|
fun fetchAndParse(url: String, httpAuth: HTTPAuthCredentials? = null): NetworkResult<ParseResult> {
|
|
val fetchResult = fetch(url, httpAuth)
|
|
|
|
return fetchResult.flatMap { result ->
|
|
try {
|
|
val parseResult = FeedParser.parse(result.feedXml, url)
|
|
NetworkResult.Success(parseResult)
|
|
} catch (e: Exception) {
|
|
NetworkResult.Failure(NetworkError.Unknown(e))
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun fetchSingleAttempt(
|
|
url: String,
|
|
httpAuth: HTTPAuthCredentials? = null,
|
|
ifNoneMatch: String? = null,
|
|
ifModifiedSince: String? = null
|
|
): NetworkResult<FetchResult> {
|
|
val requestBuilder = Request.Builder()
|
|
.url(url)
|
|
.addHeader("User-Agent", "RSSuper/1.0")
|
|
|
|
ifNoneMatch?.let { requestBuilder.addHeader("If-None-Match", it) }
|
|
ifModifiedSince?.let { requestBuilder.addHeader("If-Modified-Since", it) }
|
|
|
|
httpAuth?.let {
|
|
requestBuilder.addHeader("Authorization", it.toCredentials())
|
|
}
|
|
|
|
val request = requestBuilder.build()
|
|
|
|
return try {
|
|
val response = client.newCall(request).execute()
|
|
handleResponse(response, url)
|
|
} catch (e: IOException) {
|
|
NetworkResult.Failure(NetworkError.Unknown(e))
|
|
} catch (e: Exception) {
|
|
NetworkResult.Failure(NetworkError.Unknown(e))
|
|
}
|
|
}
|
|
|
|
private fun handleResponse(response: Response, url: String): NetworkResult<FetchResult> {
|
|
try {
|
|
val body = response.body
|
|
|
|
return when (response.code) {
|
|
200 -> {
|
|
if (body != null) {
|
|
NetworkResult.Success(FetchResult.fromResponse(response, url, response.cacheResponse != null))
|
|
} else {
|
|
NetworkResult.Failure(NetworkError.Http(response.code, "Empty response body"))
|
|
}
|
|
}
|
|
304 -> {
|
|
if (body != null) {
|
|
NetworkResult.Success(FetchResult.fromResponse(response, url, true))
|
|
} else {
|
|
NetworkResult.Failure(NetworkError.Http(response.code, "Empty response body"))
|
|
}
|
|
}
|
|
in 400..499 -> {
|
|
NetworkResult.Failure(NetworkError.Http(response.code, "Client error: ${response.message}"))
|
|
}
|
|
in 500..599 -> {
|
|
NetworkResult.Failure(NetworkError.Http(response.code, "Server error: ${response.message}"))
|
|
}
|
|
else -> {
|
|
NetworkResult.Failure(NetworkError.Http(response.code, "Unexpected status code: ${response.code}"))
|
|
}
|
|
}
|
|
} finally {
|
|
response.close()
|
|
}
|
|
}
|
|
|
|
private fun calculateBackoffDelay(attempt: Int): Long {
|
|
var delay = baseRetryDelayMs
|
|
for (i in 1 until attempt) {
|
|
delay *= 2
|
|
}
|
|
return delay
|
|
}
|
|
|
|
private class TimeoutEventListener(private val call: Call) : EventListener() {
|
|
override fun callStart(call: Call) {
|
|
}
|
|
|
|
override fun callEnd(call: Call) {
|
|
}
|
|
|
|
override fun callFailed(call: Call, ioe: IOException) {
|
|
}
|
|
}
|
|
|
|
sealed class NetworkResult<out T> {
|
|
data class Success<T>(val value: T) : NetworkResult<T>()
|
|
data class Failure<T>(val error: Throwable) : NetworkResult<T>()
|
|
|
|
fun isSuccess(): Boolean = this is Success
|
|
fun isFailure(): Boolean = this is Failure
|
|
|
|
fun getOrNull(): T? = when (this) {
|
|
is Success -> value
|
|
is Failure -> null
|
|
}
|
|
|
|
fun <R> map(transform: (T) -> R): NetworkResult<R> = when (this) {
|
|
is Success -> Success(transform(value))
|
|
is Failure -> Failure(error)
|
|
}
|
|
|
|
fun <R> flatMap(transform: (T) -> NetworkResult<R>): NetworkResult<R> = when (this) {
|
|
is Success -> transform(value)
|
|
is Failure -> Failure(error)
|
|
}
|
|
}
|
|
}
|