11package app.nubrick.nubrick.data
22
3- import app.nubrick.nubrick.data.user.syncDateFromHttpResponse
3+ import app.nubrick.nubrick.data.user.syncDateFromHttpDateHeader
4+ import app.nubrick.nubrick.schema.ApiHttpRequest
5+ import app.nubrick.nubrick.schema.ApiHttpRequestMethod
46import kotlinx.coroutines.CoroutineScope
57import kotlinx.coroutines.Dispatchers
68import kotlinx.coroutines.launch
9+ import okhttp3.MediaType.Companion.toMediaType
10+ import okhttp3.OkHttpClient
11+ import okhttp3.Request
12+ import okhttp3.RequestBody.Companion.toRequestBody
13+ import okhttp3.ResponseBody
714import java.io.ByteArrayOutputStream
815import java.io.IOException
916import java.io.InputStream
10- import java.net.HttpURLConnection
1117import java.net.SocketTimeoutException
12- import java.net.URL
1318
1419internal const val CONNECT_TIMEOUT = 10 * 1000
1520internal const val READ_TIMEOUT = 5 * 1000
21+ private const val HTTP_OK = 200
22+ private const val HTTP_NOT_FOUND = 404
1623private const val MAX_RETRIES = 2
1724private val RETRY_DELAYS = longArrayOf(1000 , 2000 )
1825private const val MAX_RESPONSE_SIZE = 5 * 1024 * 1024
1926private const val MAX_ERROR_BODY_SIZE = 4 * 1024
27+ private val JSON_MEDIA_TYPE = " application/json" .toMediaType()
2028
2129internal class HttpException (val statusCode : Int , body : String? ) :
2230 Exception (" HTTP $statusCode " + if (body.isNullOrBlank()) " " else " : $body " )
2331
2432internal class NetworkRepository (
2533 private val scope : CoroutineScope ,
2634 private val cache : CacheStore ,
35+ private val client : OkHttpClient ,
2736) {
2837 fun getWithCache (endpoint : String , syncDateTime : Boolean = false): Result <String > {
2938 val cached = cache.get(endpoint).getOrElse {
30- val result = getRequest(endpoint, syncDateTime).getOrElse { error ->
39+ val result = getRequest(endpoint, syncDateTime, client ).getOrElse { error ->
3140 return Result .failure(error)
3241 }
3342 cache.set(endpoint, result).getOrNull()
3443 return Result .success(result)
3544 }
3645 if (cached.isStale()) {
3746 scope.launch(Dispatchers .IO ) {
38- val result = getRequest(endpoint, syncDateTime).getOrNull() ? : return @launch
47+ val result = getRequest(endpoint, syncDateTime, client ).getOrNull() ? : return @launch
3948 cache.set(endpoint, result).getOrNull()
4049 }
4150 }
@@ -60,9 +69,9 @@ private fun readStream(stream: InputStream, maxSize: Int = MAX_RESPONSE_SIZE): S
6069 }
6170}
6271
63- private fun readErrorBody (connection : HttpURLConnection ): String? {
72+ private fun readErrorBody (body : ResponseBody ? ): String? {
6473 return try {
65- connection.errorStream ?.let { readStream(it, MAX_ERROR_BODY_SIZE ) }
74+ body?.byteStream() ?.let { readStream(it, MAX_ERROR_BODY_SIZE ) }
6675 } catch (_: Exception ) {
6776 null
6877 }
@@ -72,131 +81,103 @@ private fun isRetryable(e: Throwable): Boolean {
7281 return e is SocketTimeoutException || (e is HttpException && e.statusCode >= 500 )
7382}
7483
75- internal fun getRequest (endpoint : String , syncDateTime : Boolean = false): Result <String > {
84+ internal fun getRequest (
85+ endpoint : String ,
86+ syncDateTime : Boolean = false,
87+ client : OkHttpClient
88+ ): Result <String > = getRequestWithRetry {
89+ try {
90+ val t0 = System .currentTimeMillis()
91+ val request = Request .Builder ()
92+ .url(endpoint)
93+ .get()
94+ .build()
95+ executeRequest(client, request, syncDateTime, t0)
96+ } catch (e: IllegalArgumentException ) {
97+ Result .failure(e)
98+ }
99+ }
100+
101+ private fun getRequestWithRetry (
102+ request : () -> Result <String >
103+ ): Result <String > {
76104 var lastResult: Result <String > = Result .failure(IOException (" No attempts made" ))
77105 for (attempt in 0 .. MAX_RETRIES ) {
78106 if (attempt > 0 ) Thread .sleep(RETRY_DELAYS [attempt - 1 ])
79- lastResult = getRequestOnce(endpoint, syncDateTime )
107+ lastResult = request( )
80108 if (lastResult.isSuccess) return lastResult
81109 val error = lastResult.exceptionOrNull() ? : break
82110 if (! isRetryable(error)) break
83111 }
84112 return lastResult
85113}
86114
87- private fun getRequestOnce (endpoint : String , syncDateTime : Boolean ): Result <String > {
88- var connection: HttpURLConnection ? = null
89- try {
90- val t0 = System .currentTimeMillis()
91- val url = URL (endpoint)
92- connection = url.openConnection() as HttpURLConnection
93- connection.connectTimeout = CONNECT_TIMEOUT
94- connection.readTimeout = READ_TIMEOUT
95- connection.requestMethod = " GET"
96- connection.doOutput = false
97- connection.doInput = true
98- connection.useCaches = true
99- connection.connect()
100- val responseCode = connection.responseCode
101-
102- if (syncDateTime) {
103- syncDateFromHttpResponse(t0, connection)
104- }
105-
106- if (responseCode == HttpURLConnection .HTTP_OK ) {
107- return Result .success(readStream(connection.inputStream))
108- } else if (responseCode == HttpURLConnection .HTTP_NOT_FOUND ) {
109- return Result .failure(NotFoundException ())
110- } else {
111- return Result .failure(HttpException (responseCode, readErrorBody(connection)))
112- }
113- } catch (e: IOException ) {
115+ internal fun postRequest (endpoint : String , data : String , client : OkHttpClient ): Result <String > {
116+ val request = try {
117+ Request .Builder ()
118+ .url(endpoint)
119+ .post(data.toRequestBody(JSON_MEDIA_TYPE ))
120+ .build()
121+ } catch (e: IllegalArgumentException ) {
114122 return Result .failure(e)
115- } finally {
116- connection?.disconnect()
117123 }
124+ return executeRequest(client, request)
118125}
119126
120- internal fun postRequest (endpoint : String , data : String ): Result <String > {
121- var connection: HttpURLConnection ? = null
122- try {
123- val url = URL (endpoint)
124- connection = url.openConnection() as HttpURLConnection
125- connection.connectTimeout = CONNECT_TIMEOUT
126- connection.readTimeout = READ_TIMEOUT
127- connection.requestMethod = " POST"
128- connection.doOutput = true
129- connection.doInput = true
130- connection.useCaches = false
131- connection.setRequestProperty(" Content-Type" , " application/json" )
132-
133- val bodyData = data.toByteArray()
134- connection.setRequestProperty(" Content-Length" , bodyData.size.toString())
135- connection.outputStream.use { outputStream ->
136- outputStream.write(bodyData)
137- outputStream.flush()
138- }
139-
140- connection.connect()
127+ internal fun sendHttpRequest (req : ApiHttpRequest , client : OkHttpClient ): Result <String > {
128+ val url = req.url ? : return Result .failure(SkipHttpRequestException ())
129+ val method = req.method ? : ApiHttpRequestMethod .GET
130+ if (method == ApiHttpRequestMethod .UNKNOWN ) {
131+ return Result .failure(IllegalArgumentException (" Unsupported HTTP method: UNKNOWN" ))
132+ }
133+ val request = try {
134+ val builder = Request .Builder ().url(url)
141135
142- val responseCode = connection.responseCode
143- if (responseCode == HttpURLConnection .HTTP_OK ) {
144- return Result .success(readStream(connection.inputStream))
145- } else if (responseCode == HttpURLConnection .HTTP_NOT_FOUND ) {
146- return Result .failure(NotFoundException ())
147- } else {
148- return Result .failure(HttpException (responseCode, readErrorBody(connection)))
136+ req.headers?.forEach { header ->
137+ val name = header.name ? : return @forEach
138+ builder.header(name, header.value ? : " " )
149139 }
150140
151- } catch (e: IOException ) {
152- return Result .failure(e)
153- } finally {
154- connection?.disconnect()
155- }
156- }
157-
158- internal fun createHttpUrlConnection (endpoint : String ): Result <HttpURLConnection > {
159- try {
160- val parsed = URL (endpoint)
161- if (parsed.protocol != " https" && parsed.protocol != " http" ) {
162- return Result .failure(IOException (" Unsupported URL scheme: ${parsed.protocol} " ))
141+ val body = if (method != ApiHttpRequestMethod .GET &&
142+ method != ApiHttpRequestMethod .HEAD &&
143+ method != ApiHttpRequestMethod .TRACE
144+ ) {
145+ (req.body ? : " " ).toRequestBody(JSON_MEDIA_TYPE )
146+ } else {
147+ null
163148 }
164- val connection = parsed.openConnection() as HttpURLConnection
165- connection.connectTimeout = CONNECT_TIMEOUT
166- connection.readTimeout = READ_TIMEOUT
167- return Result .success(connection)
168- } catch (e: Exception ) {
149+ builder.method(method.toString(), body).build()
150+ } catch (e: IllegalArgumentException ) {
169151 return Result .failure(e)
170152 }
171- }
172-
173- internal fun setBody (connection : HttpURLConnection , body : String ) {
174- connection.doOutput = true
175- connection.useCaches = false
176- connection.setRequestProperty(" Content-Type" , " application/json" )
177153
178- val bodyData = body.toByteArray()
179- connection.setRequestProperty(" Content-Length" , bodyData.size.toString())
180- connection.outputStream.use { outputStream ->
181- outputStream.write(bodyData)
182- outputStream.flush()
183- }
154+ return executeRequest(client, request)
184155}
185156
186- internal fun connectAndGetResponse (connection : HttpURLConnection ): Result <String > {
157+ private fun executeRequest (
158+ client : OkHttpClient ,
159+ request : Request ,
160+ syncDateTime : Boolean = false,
161+ t0 : Long = System .currentTimeMillis(),
162+ ): Result <String > {
187163 try {
188- connection.connect()
189- val responseCode = connection.responseCode
190- if (responseCode == HttpURLConnection .HTTP_OK ) {
191- return Result .success(readStream(connection.inputStream))
192- } else if (responseCode == HttpURLConnection .HTTP_NOT_FOUND ) {
193- return Result .failure(NotFoundException ())
194- } else {
195- return Result .failure(HttpException (responseCode, readErrorBody(connection)))
164+ client.newCall(request).execute().use { response ->
165+ if (syncDateTime && response.networkResponse != null ) {
166+ syncDateFromHttpDateHeader(t0, System .currentTimeMillis(), response.header(" Date" ))
167+ }
168+
169+ return when (response.code) {
170+ HTTP_OK -> {
171+ val body = response.body ? : return Result .failure(IOException (" Empty response body" ))
172+ Result .success(readStream(body.byteStream()))
173+ }
174+ HTTP_NOT_FOUND -> Result .failure(NotFoundException ())
175+ else -> Result .failure(HttpException (response.code, readErrorBody(response.body)))
176+ }
196177 }
197178 } catch (e: IOException ) {
198179 return Result .failure(e)
199- } finally {
200- connection.disconnect( )
180+ } catch (e : IllegalArgumentException ) {
181+ return Result .failure(e )
201182 }
202183}
0 commit comments