From 597cec306455678f2cfe2ad2f632688292353b9e Mon Sep 17 00:00:00 2001 From: jobobby04 Date: Fri, 4 Jun 2021 18:50:22 -0400 Subject: [PATCH] Legacy backup conversion to Kotlin Serialization (#5282) * Legacy backup conversion to Kotlin Serialization * Fix BackupTest compiling --- .../data/backup/legacy/LegacyBackupManager.kt | 61 ++++++++------- .../data/backup/legacy/LegacyBackupRestore.kt | 78 +++++++++---------- .../legacy/LegacyBackupRestoreValidator.kt | 38 +++++---- .../data/backup/legacy/models/Backup.kt | 44 +++++++---- .../legacy/serializer/CategoryTypeAdapter.kt | 31 -------- .../serializer/CategoryTypeSerializer.kt | 49 ++++++++++++ .../legacy/serializer/ChapterTypeAdapter.kt | 59 -------------- .../serializer/ChapterTypeSerializer.kt | 66 ++++++++++++++++ .../legacy/serializer/HistoryTypeAdapter.kt | 32 -------- .../serializer/HistoryTypeSerializer.kt | 41 ++++++++++ .../legacy/serializer/MangaTypeAdapter.kt | 37 --------- .../legacy/serializer/MangaTypeSerializer.kt | 56 +++++++++++++ .../legacy/serializer/TrackTypeAdapter.kt | 59 -------------- .../legacy/serializer/TrackTypeSerializer.kt | 67 ++++++++++++++++ .../tachiyomi/data/backup/BackupTest.kt | 63 ++++++--------- 15 files changed, 416 insertions(+), 365 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeSerializer.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeSerializer.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeSerializer.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeSerializer.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeSerializer.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt index 8998f4baf..d03e91a7a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt @@ -2,44 +2,52 @@ package eu.kanade.tachiyomi.data.backup.legacy import android.content.Context import android.net.Uri -import com.github.salomonbrys.kotson.fromJson -import com.github.salomonbrys.kotson.registerTypeAdapter -import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import com.google.gson.JsonArray import eu.kanade.tachiyomi.data.backup.AbstractBackupManager -import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION +import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.Companion.CURRENT_VERSION import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory -import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter -import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter -import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter -import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter -import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter -import eu.kanade.tachiyomi.data.database.models.CategoryImpl +import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryImplTypeSerializer +import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeSerializer +import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterImplTypeSerializer +import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeSerializer +import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeSerializer +import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaImplTypeSerializer +import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeSerializer +import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackImplTypeSerializer +import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeSerializer +import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.ChapterImpl import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaCategory -import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.toSManga +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual import kotlin.math.max class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) { - val parser: Gson = when (version) { - 2 -> GsonBuilder() - .registerTypeAdapter(MangaTypeAdapter.build()) - .registerTypeHierarchyAdapter(ChapterTypeAdapter.build()) - .registerTypeAdapter(CategoryTypeAdapter.build()) - .registerTypeAdapter(HistoryTypeAdapter.build()) - .registerTypeHierarchyAdapter(TrackTypeAdapter.build()) - .create() + val parser: Json = when (version) { + 2 -> Json { + // Forks may have added items to backup + ignoreUnknownKeys = true + + // Register custom serializers + serializersModule = SerializersModule { + contextual(MangaTypeSerializer) + contextual(MangaImplTypeSerializer) + contextual(ChapterTypeSerializer) + contextual(ChapterImplTypeSerializer) + contextual(CategoryTypeSerializer) + contextual(CategoryImplTypeSerializer) + contextual(TrackTypeSerializer) + contextual(TrackImplTypeSerializer) + contextual(HistoryTypeSerializer) + } + } else -> throw Exception("Unknown backup version") } @@ -79,12 +87,11 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab /** * Restore the categories from Json * - * @param jsonCategories array containing categories + * @param backupCategories array containing categories */ - internal fun restoreCategories(jsonCategories: JsonArray) { + internal fun restoreCategories(backupCategories: List) { // Get categories from file and from db val dbCategories = databaseHelper.getCategories().executeAsBlocking() - val backupCategories = parser.fromJson>(jsonCategories) // Iterate over them backupCategories.forEach { category -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestore.kt index 5d11f8c5a..19c73228f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestore.kt @@ -2,88 +2,80 @@ package eu.kanade.tachiyomi.data.backup.legacy import android.content.Context import android.net.Uri -import com.github.salomonbrys.kotson.fromJson -import com.google.gson.JsonArray -import com.google.gson.JsonElement -import com.google.gson.JsonObject -import com.google.gson.JsonParser -import com.google.gson.stream.JsonReader import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore import eu.kanade.tachiyomi.data.backup.BackupNotifier import eu.kanade.tachiyomi.data.backup.legacy.models.Backup -import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGAS import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory +import eu.kanade.tachiyomi.data.backup.legacy.models.MangaObject +import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.ChapterImpl import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.source.Source +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonPrimitive +import okio.buffer +import okio.source import java.util.Date class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore(context, notifier) { override suspend fun performRestore(uri: Uri): Boolean { - val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader()) - val json = JsonParser.parseReader(reader).asJsonObject + // Read the json and create a Json Object, + // cannot use the backupManager json deserializer one because its not initialized yet + val backupObject = Json.decodeFromString( + context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() } + ) - val version = json.get(Backup.VERSION)?.asInt ?: 1 + // Get parser version + val version = backupObject["version"]?.jsonPrimitive?.intOrNull ?: 1 + + // Initialize manager backupManager = LegacyBackupManager(context, version) - val mangasJson = json.get(MANGAS).asJsonArray - restoreAmount = mangasJson.size() + 1 // +1 for categories + // Decode the json object to a Backup object + val backup = backupManager.parser.decodeFromJsonElement(backupObject) + + restoreAmount = backup.mangas.size + 1 // +1 for categories // Restore categories - json.get(Backup.CATEGORIES)?.let { restoreCategories(it) } + backup.categories?.let { restoreCategories(it) } // Store source mapping for error messages - sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json) + sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(backup.extensions ?: emptyList()) // Restore individual manga - mangasJson.forEach { + backup.mangas.forEach { if (job?.isActive != true) { return false } - restoreManga(it.asJsonObject) + restoreManga(it) } return true } - private fun restoreCategories(categoriesJson: JsonElement) { + private fun restoreCategories(categoriesJson: List) { db.inTransaction { - backupManager.restoreCategories(categoriesJson.asJsonArray) + backupManager.restoreCategories(categoriesJson) } restoreProgress += 1 showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories)) } - private suspend fun restoreManga(mangaJson: JsonObject) { - val manga = backupManager.parser.fromJson( - mangaJson.get( - Backup.MANGA - ) - ) - val chapters = backupManager.parser.fromJson>( - mangaJson.get(Backup.CHAPTERS) - ?: JsonArray() - ) - val categories = backupManager.parser.fromJson>( - mangaJson.get(Backup.CATEGORIES) - ?: JsonArray() - ) - val history = backupManager.parser.fromJson>( - mangaJson.get(Backup.HISTORY) - ?: JsonArray() - ) - val tracks = backupManager.parser.fromJson>( - mangaJson.get(Backup.TRACK) - ?: JsonArray() - ) + private suspend fun restoreManga(mangaJson: MangaObject) { + val manga = mangaJson.manga + val chapters = mangaJson.chapters ?: emptyList() + val categories = mangaJson.categories ?: emptyList() + val history = mangaJson.history ?: emptyList() + val tracks = mangaJson.track ?: emptyList() val source = backupManager.sourceManager.get(manga.source) val sourceName = sourceMapping[manga.source] ?: manga.source.toString() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestoreValidator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestoreValidator.kt index ab757059c..ac6a83bc2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestoreValidator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestoreValidator.kt @@ -2,12 +2,12 @@ package eu.kanade.tachiyomi.data.backup.legacy import android.content.Context import android.net.Uri -import com.google.gson.JsonObject -import com.google.gson.JsonParser -import com.google.gson.stream.JsonReader import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator import eu.kanade.tachiyomi.data.backup.legacy.models.Backup +import kotlinx.serialization.decodeFromString +import okio.buffer +import okio.source class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() { /** @@ -17,30 +17,30 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() { * @return List of missing sources or missing trackers. */ override fun validate(context: Context, uri: Uri): Results { - val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader()) - val json = JsonParser.parseReader(reader).asJsonObject + val backupManager = LegacyBackupManager(context) - val version = json.get(Backup.VERSION) - val mangasJson = json.get(Backup.MANGAS) - if (version == null || mangasJson == null) { + val backup = backupManager.parser.decodeFromString( + context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() } + ) + + if (backup.version == null) { throw Exception(context.getString(R.string.invalid_backup_file_missing_data)) } - val mangas = mangasJson.asJsonArray - if (mangas.size() == 0) { + if (backup.mangas.isEmpty()) { throw Exception(context.getString(R.string.invalid_backup_file_missing_manga)) } - val sources = getSourceMapping(json) + val sources = getSourceMapping(backup.extensions ?: emptyList()) val missingSources = sources .filter { sourceManager.get(it.key) == null } .values .sorted() - val trackers = mangas - .filter { it.asJsonObject.has("track") } - .flatMap { it.asJsonObject["track"].asJsonArray } - .map { it.asJsonObject["s"].asInt } + val trackers = backup.mangas + .filterNot { it.track.isNullOrEmpty() } + .flatMap { it.track ?: emptyList() } + .map { it.sync_id } .distinct() val missingTrackers = trackers .mapNotNull { trackManager.getService(it) } @@ -52,12 +52,10 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() { } companion object { - fun getSourceMapping(json: JsonObject): Map { - val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap() - - return extensionsMapping.asJsonArray + fun getSourceMapping(extensionsMapping: List): Map { + return extensionsMapping .map { - val items = it.asString.split(":") + val items = it.split(":") items[0].toLong() to items[1] } .toMap() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/models/Backup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/models/Backup.kt index 32dfa9245..2766eeecc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/models/Backup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/models/Backup.kt @@ -1,25 +1,37 @@ package eu.kanade.tachiyomi.data.backup.legacy.models +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.Track +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * Json values - */ -object Backup { - const val CURRENT_VERSION = 2 - const val MANGA = "manga" - const val MANGAS = "mangas" - const val TRACK = "track" - const val CHAPTERS = "chapters" - const val CATEGORIES = "categories" - const val EXTENSIONS = "extensions" - const val HISTORY = "history" - const val VERSION = "version" +@Serializable +data class Backup( + val version: Int? = null, + var mangas: MutableList = mutableListOf(), + var categories: List<@Contextual Category>? = null, + var extensions: List? = null +) { + companion object { + const val CURRENT_VERSION = 2 - fun getDefaultFilename(): String { - val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date()) - return "tachiyomi_$date.json" + fun getDefaultFilename(): String { + val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date()) + return "tachiyomi_$date.json" + } } } + +@Serializable +data class MangaObject( + var manga: @Contextual Manga, + var chapters: List<@Contextual Chapter>? = null, + var categories: List? = null, + var track: List<@Contextual Track>? = null, + var history: List<@Contextual DHistory>? = null +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeAdapter.kt deleted file mode 100644 index d346af19c..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeAdapter.kt +++ /dev/null @@ -1,31 +0,0 @@ -package eu.kanade.tachiyomi.data.backup.legacy.serializer - -import com.github.salomonbrys.kotson.typeAdapter -import com.google.gson.TypeAdapter -import eu.kanade.tachiyomi.data.database.models.CategoryImpl - -/** - * JSON Serializer used to write / read [CategoryImpl] to / from json - */ -object CategoryTypeAdapter { - - fun build(): TypeAdapter { - return typeAdapter { - write { - beginArray() - value(it.name) - value(it.order) - endArray() - } - - read { - beginArray() - val category = CategoryImpl() - category.name = nextString() - category.order = nextInt() - endArray() - category - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeSerializer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeSerializer.kt new file mode 100644 index 000000000..b5f52a1bb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeSerializer.kt @@ -0,0 +1,49 @@ +package eu.kanade.tachiyomi.data.backup.legacy.serializer + +import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.data.database.models.CategoryImpl +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive + +/** + * JSON Serializer used to write / read [CategoryImpl] to / from json + */ +open class CategoryBaseSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Category") + + override fun serialize(encoder: Encoder, value: T) { + encoder as JsonEncoder + encoder.encodeJsonElement( + buildJsonArray { + add(value.name) + add(value.order) + } + ) + } + + @Suppress("UNCHECKED_CAST") + override fun deserialize(decoder: Decoder): T { + // make a category impl and cast as T so that the serializer accepts it + return CategoryImpl().apply { + decoder as JsonDecoder + val array = decoder.decodeJsonElement().jsonArray + name = array[0].jsonPrimitive.content + order = array[1].jsonPrimitive.int + } as T + } +} + +// Allow for serialization of a category and category impl +object CategoryTypeSerializer : CategoryBaseSerializer() + +object CategoryImplTypeSerializer : CategoryBaseSerializer() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeAdapter.kt deleted file mode 100644 index cacc8cb25..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeAdapter.kt +++ /dev/null @@ -1,59 +0,0 @@ -package eu.kanade.tachiyomi.data.backup.legacy.serializer - -import com.github.salomonbrys.kotson.typeAdapter -import com.google.gson.TypeAdapter -import com.google.gson.stream.JsonToken -import eu.kanade.tachiyomi.data.database.models.ChapterImpl - -/** - * JSON Serializer used to write / read [ChapterImpl] to / from json - */ -object ChapterTypeAdapter { - - private const val URL = "u" - private const val READ = "r" - private const val BOOKMARK = "b" - private const val LAST_READ = "l" - - fun build(): TypeAdapter { - return typeAdapter { - write { - if (it.read || it.bookmark || it.last_page_read != 0) { - beginObject() - name(URL) - value(it.url) - if (it.read) { - name(READ) - value(1) - } - if (it.bookmark) { - name(BOOKMARK) - value(1) - } - if (it.last_page_read != 0) { - name(LAST_READ) - value(it.last_page_read) - } - endObject() - } - } - - read { - val chapter = ChapterImpl() - beginObject() - while (hasNext()) { - if (peek() == JsonToken.NAME) { - when (nextName()) { - URL -> chapter.url = nextString() - READ -> chapter.read = nextInt() == 1 - BOOKMARK -> chapter.bookmark = nextInt() == 1 - LAST_READ -> chapter.last_page_read = nextInt() - } - } - } - endObject() - chapter - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeSerializer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeSerializer.kt new file mode 100644 index 000000000..02f52362f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeSerializer.kt @@ -0,0 +1,66 @@ +package eu.kanade.tachiyomi.data.backup.legacy.serializer + +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.ChapterImpl +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put + +/** + * JSON Serializer used to write / read [ChapterImpl] to / from json + */ +open class ChapterBaseSerializer : KSerializer { + + override val descriptor = buildClassSerialDescriptor("Chapter") + + override fun serialize(encoder: Encoder, value: T) { + encoder as JsonEncoder + encoder.encodeJsonElement( + buildJsonObject { + put(URL, value.url) + if (value.read) { + put(READ, 1) + } + if (value.bookmark) { + put(BOOKMARK, 1) + } + if (value.last_page_read != 0) { + put(LAST_READ, value.last_page_read) + } + } + ) + } + + @Suppress("UNCHECKED_CAST") + override fun deserialize(decoder: Decoder): T { + // make a chapter impl and cast as T so that the serializer accepts it + return ChapterImpl().apply { + decoder as JsonDecoder + val jsonObject = decoder.decodeJsonElement().jsonObject + url = jsonObject[URL]!!.jsonPrimitive.content + read = jsonObject[READ]?.jsonPrimitive?.intOrNull == 1 + bookmark = jsonObject[BOOKMARK]?.jsonPrimitive?.intOrNull == 1 + last_page_read = jsonObject[LAST_READ]?.jsonPrimitive?.intOrNull ?: last_page_read + } as T + } + + companion object { + private const val URL = "u" + private const val READ = "r" + private const val BOOKMARK = "b" + private const val LAST_READ = "l" + } +} + +// Allow for serialization of a chapter and chapter impl +object ChapterTypeSerializer : ChapterBaseSerializer() + +object ChapterImplTypeSerializer : ChapterBaseSerializer() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeAdapter.kt deleted file mode 100644 index 4f7d5d9ff..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeAdapter.kt +++ /dev/null @@ -1,32 +0,0 @@ -package eu.kanade.tachiyomi.data.backup.legacy.serializer - -import com.github.salomonbrys.kotson.typeAdapter -import com.google.gson.TypeAdapter -import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory - -/** - * JSON Serializer used to write / read [DHistory] to / from json - */ -object HistoryTypeAdapter { - - fun build(): TypeAdapter { - return typeAdapter { - write { - if (it.lastRead != 0L) { - beginArray() - value(it.url) - value(it.lastRead) - endArray() - } - } - - read { - beginArray() - val url = nextString() - val lastRead = nextLong() - endArray() - DHistory(url, lastRead) - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeSerializer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeSerializer.kt new file mode 100644 index 000000000..ba19803ed --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeSerializer.kt @@ -0,0 +1,41 @@ +package eu.kanade.tachiyomi.data.backup.legacy.serializer + +import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long + +/** + * JSON Serializer used to write / read [DHistory] to / from json + */ +object HistoryTypeSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("History") + + override fun serialize(encoder: Encoder, value: DHistory) { + encoder as JsonEncoder + encoder.encodeJsonElement( + buildJsonArray { + add(value.url) + add(value.lastRead) + } + ) + } + + override fun deserialize(decoder: Decoder): DHistory { + decoder as JsonDecoder + val array = decoder.decodeJsonElement().jsonArray + return DHistory( + url = array[0].jsonPrimitive.content, + lastRead = array[1].jsonPrimitive.long + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeAdapter.kt deleted file mode 100644 index aaab4c76f..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeAdapter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package eu.kanade.tachiyomi.data.backup.legacy.serializer - -import com.github.salomonbrys.kotson.typeAdapter -import com.google.gson.TypeAdapter -import eu.kanade.tachiyomi.data.database.models.MangaImpl - -/** - * JSON Serializer used to write / read [MangaImpl] to / from json - */ -object MangaTypeAdapter { - - fun build(): TypeAdapter { - return typeAdapter { - write { - beginArray() - value(it.url) - value(it.title) - value(it.source) - value(it.viewer_flags) - value(it.chapter_flags) - endArray() - } - - read { - beginArray() - val manga = MangaImpl() - manga.url = nextString() - manga.title = nextString() - manga.source = nextLong() - manga.viewer_flags = nextInt() - manga.chapter_flags = nextInt() - endArray() - manga - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeSerializer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeSerializer.kt new file mode 100644 index 000000000..1224b3cd5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeSerializer.kt @@ -0,0 +1,56 @@ +package eu.kanade.tachiyomi.data.backup.legacy.serializer + +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaImpl +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long + +/** + * JSON Serializer used to write / read [MangaImpl] to / from json + */ +open class MangaBaseSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Manga") + + override fun serialize(encoder: Encoder, value: T) { + encoder as JsonEncoder + encoder.encodeJsonElement( + buildJsonArray { + add(value.url) + add(value.title) + add(value.source) + add(value.viewer_flags) + add(value.chapter_flags) + } + ) + } + + @Suppress("UNCHECKED_CAST") + override fun deserialize(decoder: Decoder): T { + // make a manga impl and cast as T so that the serializer accepts it + return MangaImpl().apply { + decoder as JsonDecoder + val array = decoder.decodeJsonElement().jsonArray + url = array[0].jsonPrimitive.content + title = array[1].jsonPrimitive.content + source = array[2].jsonPrimitive.long + viewer_flags = array[3].jsonPrimitive.int + chapter_flags = array[4].jsonPrimitive.int + } as T + } +} + +// Allow for serialization of a manga and manga impl +object MangaTypeSerializer : MangaBaseSerializer() + +object MangaImplTypeSerializer : MangaBaseSerializer() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeAdapter.kt deleted file mode 100644 index 84c0cd829..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeAdapter.kt +++ /dev/null @@ -1,59 +0,0 @@ -package eu.kanade.tachiyomi.data.backup.legacy.serializer - -import com.github.salomonbrys.kotson.typeAdapter -import com.google.gson.TypeAdapter -import com.google.gson.stream.JsonToken -import eu.kanade.tachiyomi.data.database.models.TrackImpl - -/** - * JSON Serializer used to write / read [TrackImpl] to / from json - */ -object TrackTypeAdapter { - - private const val SYNC = "s" - private const val MEDIA = "r" - private const val LIBRARY = "ml" - private const val TITLE = "t" - private const val LAST_READ = "l" - private const val TRACKING_URL = "u" - - fun build(): TypeAdapter { - return typeAdapter { - write { - beginObject() - name(TITLE) - value(it.title) - name(SYNC) - value(it.sync_id) - name(MEDIA) - value(it.media_id) - name(LIBRARY) - value(it.library_id) - name(LAST_READ) - value(it.last_chapter_read) - name(TRACKING_URL) - value(it.tracking_url) - endObject() - } - - read { - val track = TrackImpl() - beginObject() - while (hasNext()) { - if (peek() == JsonToken.NAME) { - when (nextName()) { - TITLE -> track.title = nextString() - SYNC -> track.sync_id = nextInt() - MEDIA -> track.media_id = nextInt() - LIBRARY -> track.library_id = nextLong() - LAST_READ -> track.last_chapter_read = nextInt() - TRACKING_URL -> track.tracking_url = nextString() - } - } - } - endObject() - track - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeSerializer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeSerializer.kt new file mode 100644 index 000000000..8a08f79ff --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeSerializer.kt @@ -0,0 +1,67 @@ +package eu.kanade.tachiyomi.data.backup.legacy.serializer + +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.database.models.TrackImpl +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlinx.serialization.json.put + +/** + * JSON Serializer used to write / read [TrackImpl] to / from json + */ +open class TrackBaseSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Track") + + override fun serialize(encoder: Encoder, value: T) { + encoder as JsonEncoder + encoder.encodeJsonElement( + buildJsonObject { + put(TITLE, value.title) + put(SYNC, value.sync_id) + put(MEDIA, value.media_id) + put(LIBRARY, value.library_id) + put(LAST_READ, value.last_chapter_read) + put(TRACKING_URL, value.tracking_url) + } + ) + } + + @Suppress("UNCHECKED_CAST") + override fun deserialize(decoder: Decoder): T { + // make a track impl and cast as T so that the serializer accepts it + return TrackImpl().apply { + decoder as JsonDecoder + val jsonObject = decoder.decodeJsonElement().jsonObject + title = jsonObject[TITLE]!!.jsonPrimitive.content + sync_id = jsonObject[SYNC]!!.jsonPrimitive.int + media_id = jsonObject[MEDIA]!!.jsonPrimitive.int + library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long + last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.int + tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content + } as T + } + + companion object { + private const val SYNC = "s" + private const val MEDIA = "r" + private const val LIBRARY = "ml" + private const val TITLE = "t" + private const val LAST_READ = "l" + private const val TRACKING_URL = "u" + } +} + +// Allow for serialization of a track and track impl +object TrackTypeSerializer : TrackBaseSerializer() + +object TrackImplTypeSerializer : TrackBaseSerializer() diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt b/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt index d234fa7eb..c6f415265 100644 --- a/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt +++ b/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt @@ -3,9 +3,6 @@ package eu.kanade.tachiyomi.data.backup import android.app.Application import android.content.Context import android.os.Build -import com.github.salomonbrys.kotson.fromJson -import com.google.gson.JsonArray -import com.google.gson.JsonObject import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager @@ -17,12 +14,16 @@ import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.ChapterImpl import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaImpl +import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import kotlinx.coroutines.runBlocking +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.buildJsonObject import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test @@ -47,16 +48,10 @@ import uy.kohesive.injekt.api.addSingleton @RunWith(CustomRobolectricGradleTestRunner::class) class BackupTest { // Create root object - var root = JsonObject() + var root = Backup() // Create information object - var information = JsonObject() - - // Create manga array - var mangaEntries = JsonArray() - - // Create category array - var categoryEntries = JsonArray() + var information = buildJsonObject {} lateinit var app: Application lateinit var context: Context @@ -83,11 +78,6 @@ class BackupTest { source = mock(HttpSource::class.java) `when`(legacyBackupManager.sourceManager.get(anyLong())).thenReturn(source) - - root.add(Backup.MANGAS, mangaEntries) - root.add(Backup.CATEGORIES, categoryEntries) - - clearJson() } /** @@ -95,11 +85,8 @@ class BackupTest { */ @Test fun testRestoreEmptyCategory() { - // Create backup of empty database - legacyBackupManager.backupCategories(categoryEntries) - // Restore Json - legacyBackupManager.restoreCategories(categoryEntries) + legacyBackupManager.restoreCategories(root.categories ?: emptyList()) // Check if empty val dbCats = db.getCategories().executeAsBlocking() @@ -115,7 +102,7 @@ class BackupTest { val category = addSingleCategory("category") // Restore Json - legacyBackupManager.restoreCategories(categoryEntries) + legacyBackupManager.restoreCategories(root.categories ?: emptyList()) // Check if successful val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking() @@ -139,7 +126,7 @@ class BackupTest { db.insertCategory(category).executeAsBlocking() // Restore Json - legacyBackupManager.restoreCategories(categoryEntries) + legacyBackupManager.restoreCategories(root.categories ?: emptyList()) // Check if successful val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking() @@ -167,9 +154,6 @@ class BackupTest { assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.flagValue) assertThat(favoriteManga[0].orientationType).isEqualTo(OrientationType.PORTRAIT.flagValue) - // Update json with all options enabled - mangaEntries.add(legacyBackupManager.backupMangaObject(manga, 1)) - // Change manga in database to default values val dbManga = getSingleManga("One Piece") dbManga.id = manga.id @@ -198,9 +182,9 @@ class BackupTest { // Restore Json // Create JSON from manga to test parser - val json = legacyBackupManager.parser.toJsonTree(manga) + val json = legacyBackupManager.parser.encodeToString(manga) // Restore JSON from manga to test parser - val jsonManga = legacyBackupManager.parser.fromJson(json) + val jsonManga = legacyBackupManager.parser.decodeFromString(json) // Restore manga with fetch observable val networkManga = getSingleManga("One Piece") @@ -237,8 +221,8 @@ class BackupTest { } // Check parser - val chaptersJson = legacyBackupManager.parser.toJsonTree(chapters) - val restoredChapters = legacyBackupManager.parser.fromJson>(chaptersJson) + val chaptersJson = legacyBackupManager.parser.encodeToString(chapters) + val restoredChapters = legacyBackupManager.parser.decodeFromString>(chaptersJson) // Fetch chapters from upstream // Create list @@ -275,8 +259,8 @@ class BackupTest { historyList.add(historyJson) // Check parser - val historyListJson = legacyBackupManager.parser.toJsonTree(historyList) - val history = legacyBackupManager.parser.fromJson>(historyListJson) + val historyListJson = legacyBackupManager.parser.encodeToString(historyList) + val history = legacyBackupManager.parser.decodeFromString>(historyListJson) // Restore categories legacyBackupManager.restoreHistoryForManga(history) @@ -314,8 +298,8 @@ class BackupTest { // Check parser and restore already in database var trackList = listOf(track) // Check parser - var trackListJson = legacyBackupManager.parser.toJsonTree(trackList) - var trackListRestore = legacyBackupManager.parser.fromJson>(trackListJson) + var trackListJson = legacyBackupManager.parser.encodeToString(trackList) + var trackListRestore = legacyBackupManager.parser.decodeFromString>(trackListJson) legacyBackupManager.restoreTrackForManga(manga, trackListRestore) // Assert if restore works. @@ -337,8 +321,8 @@ class BackupTest { trackList = listOf(track2) // Check parser - trackListJson = legacyBackupManager.parser.toJsonTree(trackList) - trackListRestore = legacyBackupManager.parser.fromJson>(trackListJson) + trackListJson = legacyBackupManager.parser.encodeToString(trackList) + trackListRestore = legacyBackupManager.parser.decodeFromString>(trackListJson) legacyBackupManager.restoreTrackForManga(manga2, trackListRestore) // Assert if restore works. @@ -348,16 +332,13 @@ class BackupTest { } private fun clearJson() { - root = JsonObject() - information = JsonObject() - mangaEntries = JsonArray() - categoryEntries = JsonArray() + root = Backup() + information = buildJsonObject {} } private fun addSingleCategory(name: String): Category { val category = Category.create(name) - val catJson = legacyBackupManager.parser.toJsonTree(category) - categoryEntries.add(catJson) + root.categories = listOf(category) return category }