Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cacheZipResponse to repository #10

Merged
merged 1 commit into from
Aug 26, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
1 change: 1 addition & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ android {

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

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