Skip to content
Snippets Groups Projects
Unverified Commit 9bca0648 authored by Matthias Urhahn's avatar Matthias Urhahn Committed by GitHub
Browse files

Improve Gson deserialization handling (DEV) (#1715)


* Check whether the local config file exists before parsing it.

* Force types deserialized via Gson to be non-null.
Perform sanity checks and empty file deletion directly when trying to load the json file.

* Check that empty json file is deleted.

* LINTs

* Test parsing exception.

Co-authored-by: default avatarKolya Opahle <k.opahle@sap.com>
Co-authored-by: default avatarharambasicluka <64483219+harambasicluka@users.noreply.github.com>
Co-authored-by: default avatarRalf Gehrer <ralfgehrer@users.noreply.github.com>
parent e17c5649
No related branches found
No related tags found
No related merge requests found
......@@ -61,8 +61,9 @@ class AppConfigStorage @Inject constructor(
}
return@withLock try {
gson.fromJson<InternalConfigData>(configFile).also {
gson.fromJson<InternalConfigData>(configFile)?.also {
requireNotNull(it.rawData)
Timber.v("Loaded stored config, serverTime=%s", it.serverTime)
}
} catch (e: Exception) {
Timber.e(e, "Couldn't load config.")
......
......@@ -19,7 +19,7 @@ class RemoteAppConfigSource @Inject constructor(
) {
suspend fun getConfigData(): ConfigData? = withContext(dispatcherProvider.IO) {
Timber.tag(TAG).v("retrieveConfig()")
Timber.tag(TAG).v("getConfigData()")
val configDownload = try {
server.downloadAppConfig()
......
......@@ -43,13 +43,11 @@ class ExposureDetectionTrackerStorage @Inject constructor(
suspend fun load(): Map<String, TrackedExposureDetection> = mutex.withLock {
return@withLock try {
if (!storageFile.exists()) return@withLock emptyMap()
gson.fromJson<Map<String, TrackedExposureDetection>>(storageFile).also {
gson.fromJson<Map<String, TrackedExposureDetection>>(storageFile)?.also {
require(it.size >= 0)
Timber.v("Loaded detection data: %s", it)
lastCalcuationData = it
}
} ?: emptyMap()
} catch (e: Exception) {
Timber.e(e, "Failed to load tracked detections.")
if (storageFile.delete()) Timber.w("Storage file was deleted.")
......
......@@ -3,6 +3,7 @@ package de.rki.coronawarnapp.util.serialization
import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.reflect.TypeToken
import timber.log.Timber
import java.io.File
import kotlin.reflect.KClass
......@@ -11,8 +12,28 @@ inline fun <reified T> Gson.fromJson(json: String): T = fromJson(
object : TypeToken<T>() {}.type
)
inline fun <reified T> Gson.fromJson(file: File): T = file.bufferedReader().use {
fromJson(it, object : TypeToken<T>() {}.type)
/**
* Returns null if the file doesn't exist, otherwise returns the parsed object.
* Throws an exception if the object can't be parsed.
* An empty file, that was deserialized to a null value is deleted.
*/
inline fun <reified T : Any> Gson.fromJson(file: File): T? {
if (!file.exists()) {
Timber.v("fromJson(): File doesn't exist %s", file)
return null
}
return file.bufferedReader().use {
val value: T? = fromJson(it, object : TypeToken<T>() {}.type)
if (value != null) {
Timber.v("Json read from %s", file)
value
} else {
Timber.w("Tried to parse json from file that exists, but was empty: %s", file)
if (file.delete()) Timber.w("Deleted empty json file: %s", file)
null
}
}
}
inline fun <reified T> Gson.toJson(data: T, file: File) = file.bufferedWriter().use { writer ->
......
......@@ -113,11 +113,10 @@ class ExposureDetectionTrackerStorageTest : BaseIOTest() {
@Test
fun `saving data creates a json file`() = runBlockingTest {
createStorage().save(demoData)
storageFile.exists() shouldBe true
val storedData: Map<String, TrackedExposureDetection> = gson.fromJson(storageFile)
val storedData: Map<String, TrackedExposureDetection> = gson.fromJson(storageFile)!!
storedData shouldBe demoData
gson.toJson(storedData) shouldBe demoJsonString
......
package de.rki.coronawarnapp.util.serialization
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import testhelpers.BaseIOTest
import java.io.File
import java.util.UUID
class GsonExtensionsTest : BaseIOTest() {
private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName)
private val testFile = File(testDir, "testfile")
private val gson = Gson()
@BeforeEach
fun setup() {
testDir.mkdirs()
}
@AfterEach
fun teardown() {
testDir.deleteRecursively()
}
data class TestData(
val value: String
)
@Test
fun `serialize and deserialize`() {
val testData = TestData(value = UUID.randomUUID().toString())
gson.toJson(testData, testFile)
gson.fromJson<TestData>(testFile) shouldBe testData
}
@Test
fun `deserialize an empty file`() {
testFile.createNewFile()
testFile.exists() shouldBe true
val testData: TestData? = gson.fromJson(testFile)
testData shouldBe null
testFile.exists() shouldBe false
}
@Test
fun `deserialize a malformed file`() {
testFile.writeText("{")
shouldThrow<JsonSyntaxException> {
gson.fromJson(testFile)
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment