Skip to content

Commit

Permalink
Add cacheZipResponse to repository
Browse files Browse the repository at this point in the history
  • Loading branch information
ksharma-xyz committed Aug 26, 2024
1 parent de7d810 commit f6d08ab
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 89 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
package xyz.ksharma.krail.coroutines.ext

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.coroutineContext
import kotlin.Result

/**
* An extension to [kotlin.runCatching].
* Executes the given block on the specified [CoroutineDispatcher] and returns the result in a [Result] object.
*
* Calls the specified function block with this value as its receiver and returns its encapsulated
* result if invocation was successful, catching any [Throwable] exception that was thrown from the
* block function execution and encapsulating it as a failure.
* If the block execution is successful, the result is wrapped in a [Result.success].
* If an exception is thrown, it is wrapped in a [Result.Failure].
*
* Will not catch [CancellationException].
* **Note:** This function will not catch [CancellationException].
*
* @param dispatcher The CoroutineDispatcher on which to execute the block.
* @param block The block of code to execute.
* @return A [Result] object containing the result of the block execution or the exception that was thrown.
*/
suspend inline fun <T, R> T.safeResult(block: T.() -> R): Result<R> {
return try {
suspend fun <T, R> T.safeResult(
dispatcher: CoroutineDispatcher,
block: T.() -> R
): Result<R> = withContext(dispatcher) {
try {
Result.success(block())
} catch (e: Throwable) {
// Should not catch CancellationException
Expand Down
1 change: 1 addition & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ android {

dependencies {
api(projects.core.di)
implementation(projects.core.coroutinesExt)
implementation(projects.core.network)
implementation(projects.core.model)

Expand Down
34 changes: 34 additions & 0 deletions core/data/src/main/kotlin/xyz/ksharma/krail/data/FilesHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package xyz.ksharma.krail.data

import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.util.zip.ZipInputStream

/**
* Extracts a ZIP entry to a specified cache path.
*
* If the entry is a directory, it creates the directory structure.
* If the entry is a file, it copies the file contents to the cache path.
*
* **Note:** If the target file already exists, it will be overwritten.
*
* @param isDirectory Indicates whether the entry is a directory.
* @param path The target path in the cache directory.
* @param inputStream The input stream containing the ZIP entry data.
*/
internal fun writeToCacheFromZip(
isDirectory: Boolean,
path: Path,
inputStream: ZipInputStream
) {
if (isDirectory) {
Files.createDirectories(path)
} else {
// Handle creation of parent directories
if (path.parent != null && Files.notExists(path.parent)) {
Files.createDirectories(path.parent)
}
Files.copy(inputStream, path, StandardCopyOption.REPLACE_EXISTING)
}
}
51 changes: 51 additions & 0 deletions core/data/src/main/kotlin/xyz/ksharma/krail/data/ResponseExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package xyz.ksharma.krail.data

import android.content.Context
import kotlinx.coroutines.CoroutineDispatcher
import okhttp3.Response
import xyz.ksharma.krail.coroutines.ext.safeResult
import xyz.ksharma.krail.network.files.toPath
import java.io.File
import java.io.IOException
import java.nio.file.Path
import java.util.zip.ZipInputStream

/**
* Caches the content of a successful ZIP response in the cache directory associated with the
* provided context. This function operates on a specified coroutine dispatcher for asynchronous
* execution.
*
* @throws IOException If an I/O error occurs during the caching process,
* or if the response code is unexpected.
* @param dispatcher The coroutine dispatcher to use for suspending operations.
* @param context The context that provides the cache directory path.
*/
@Throws(IOException::class)
suspend fun Response.cacheZipResponse(dispatcher: CoroutineDispatcher, context: Context) =
safeResult(dispatcher) {
if (!isSuccessful) {
throw IOException("Unexpected code $code")
}

val responseBody = body!!

ZipInputStream(responseBody.byteStream()).use { inputStream ->
// List files in zip
var zipEntry = inputStream.nextEntry

while (zipEntry != null) {
val isDirectory = zipEntry.name.endsWith(File.separator)
val path: Path = context.toPath(zipEntry.name)

println("zipEntry: $zipEntry")

writeToCacheFromZip(isDirectory, path, inputStream)

zipEntry = inputStream.nextEntry
}
inputStream.closeEntry()
}
close()
}.getOrElse { error ->
println("cacheZipResponse: $error")
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import okhttp3.Response
import xyz.ksharma.krail.data.cacheZipResponse
import xyz.ksharma.krail.di.AppDispatchers
import xyz.ksharma.krail.di.Dispatcher
import xyz.ksharma.krail.model.gtfs_realtime.proto.Stop
Expand All @@ -23,18 +25,8 @@ class SydneyTrainsRepositoryImpl @Inject constructor(

override suspend fun getSydneyTrains() {
Log.d(TAG, "getSydneyTrains: ")
gtfsService.getSydneyTrainSchedule() // Zip
//println(sydneyTrainsResponse.toString(UTF_8))

//val files = extractGtfsFiles(sydneyTrainsResponse)
//Log.d(TAG, "files[${files.size}]: ${files.keys}")
//Log.d(TAG, "file[1]: ${files.values.first()}")

/*
val stopsFile:ByteArray? = files.filter { it.key == "stops.txt" }.values.firstOrNull()
Log.d(TAG, "stopsFile: ${stopsFile?.decodeToString()}")
Log.d(TAG, "parse stopsFile: ${stopsFile?.parseStops()}")
*/
val response: Response = gtfsService.getSydneyTrainSchedule()
response.cacheZipResponse(dispatcher = ioDispatcher, context = context)
}

private fun ByteArray.parseStops(): List<Stop> {
Expand All @@ -43,7 +35,8 @@ class SydneyTrainsRepositoryImpl @Inject constructor(
val lineList = reader.readLine()?.split(",") ?: return emptyList()
Log.d(TAG, "parseStops: rows - ${lineList.size}")

val columnIndices = lineList.mapIndexed { index, columnName -> columnName to index }.toMap()
val columnIndices =
lineList.mapIndexed { index, columnName -> columnName to index }.toMap()

var line: String? = reader.readLine()
Log.d(TAG, "parseStops: line - $line")
Expand All @@ -55,18 +48,22 @@ class SydneyTrainsRepositoryImpl @Inject constructor(
stop_id = tokens.getOrNull(columnIndices["stop_id"] ?: -1) ?: "",
stop_code = tokens.getOrNull(columnIndices["stop_code"] ?: -1)?.translate(),
stop_name = tokens.getOrNull(columnIndices["stop_name"] ?: -1)?.translate(),
tts_stop_name = tokens.getOrNull(columnIndices["tts_stop_name"] ?: -1)?.translate(),
tts_stop_name = tokens.getOrNull(columnIndices["tts_stop_name"] ?: -1)
?.translate(),
stop_desc = tokens.getOrNull(columnIndices["stop_desc"] ?: -1)?.translate(),
stop_lat = tokens.getOrNull(columnIndices["stop_lat"] ?: -1)?.toFloatOrNull(),
stop_lon = tokens.getOrNull(columnIndices["stop_lon"] ?: -1)?.toFloatOrNull(),
zone_id = tokens.getOrNull(columnIndices["zone_id"] ?: -1),
stop_url = tokens.getOrNull(columnIndices["stop_url"] ?: -1)?.translate(),
parent_station = tokens.getOrNull(columnIndices["parent_station"] ?: -1),
stop_timezone = tokens.getOrNull(columnIndices["stop_timezone"] ?: -1),
wheelchair_boarding = tokens.getOrNull(columnIndices["wheelchair_boarding"] ?: -1)
wheelchair_boarding = tokens.getOrNull(
columnIndices["wheelchair_boarding"] ?: -1
)
?.toIntOrNull().toWheelchairBoarding(),
level_id = tokens.getOrNull(columnIndices["level_id"] ?: -1),
platform_code = tokens.getOrNull(columnIndices["platform_code"] ?: -1)?.translate(),
platform_code = tokens.getOrNull(columnIndices["platform_code"] ?: -1)
?.translate(),
)
stops.add(stop)
line = reader.readLine()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,88 +1,25 @@
package xyz.ksharma.krail.network

import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import xyz.ksharma.krail.network.di.NetworkModule.Companion.BASE_URL
import xyz.ksharma.krail.network.files.toPath
import java.io.File
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.util.zip.ZipInputStream
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class GtfsServiceImpl @Inject constructor(
private val okHttpClient: OkHttpClient,
@ApplicationContext private val context: Context,
) : GtfsService {

private val TAG = "SydneyTrainsServiceImpl"

override suspend fun getSydneyTrainSchedule(): Response {
val request = Request.Builder()
.url("$BASE_URL/v1/gtfs/schedule/sydneytrains") // Replace with your API endpoint
.url("$BASE_URL/v1/gtfs/schedule/sydneytrains")
.build()

val response = okHttpClient.newCall(request).execute()
// don't log it's entire response body,which is huge.
// Log.d(TAG, "fetchSydneyTrains: ${response.body?.string()}")

val map = getHTMLZipOk(response)
Log.d(TAG, "filesMap: $map")

return response
}

@Throws(IOException::class)
fun getHTMLZipOk(response: Response) {
if (!response.isSuccessful) {
throw IOException("Unexpected code ${response.code}")
}

val responseBody = response.body!!

ZipInputStream(responseBody.byteStream()).use { inputStream ->
// List files in zip
var zipEntry = inputStream.nextEntry

while (zipEntry != null) {
val isDirectory = zipEntry.name.endsWith(File.separator)
val path: Path = context.toPath(zipEntry.name)

Log.d(TAG, "zipEntry: $zipEntry")

writeToCacheFromZip(isDirectory, path, inputStream)

zipEntry = inputStream.nextEntry
}
inputStream.closeEntry()
}
response.close()
}

/**
* Extract files from zip and save to a file in cache directory.
*/
private fun writeToCacheFromZip(
isDirectory: Boolean,
path: Path,
inputStream: ZipInputStream
) {
if (isDirectory) {
Files.createDirectories(path)
} else {
// Handle creation of parent directories
if (path.parent != null && Files.notExists(path.parent)) {
Files.createDirectories(path.parent)
}
Files.copy(inputStream, path, StandardCopyOption.REPLACE_EXISTING)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ internal fun Path.readFile(): ByteArray = this.toFile().readBytes()
*
* @return The file contents as a [ByteArray].
*/
internal fun Context.toPath(fileName: String): Path = cacheDir.toPath().resolve(fileName).normalize()
fun Context.toPath(fileName: String): Path = cacheDir.toPath().resolve(fileName).normalize()

0 comments on commit f6d08ab

Please sign in to comment.