From cca2b8e896ec6c3206d432737dc37c1a5633fa41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Fri, 29 Sep 2023 15:13:34 +0200 Subject: [PATCH] Update migration guide and add `Locator.fromJSON(withLegacyHref)` --- CHANGELOG.md | 4 +- docs/migration-guide.md | 49 +++++++++++++++++++ .../readium/r2/shared/publication/Locator.kt | 32 +++++++++++- .../java/org/readium/r2/shared/util/Url.kt | 17 +++++-- .../r2/shared/publication/LocatorTest.kt | 18 +++++++ .../org/readium/r2/shared/util/UrlTest.kt | 3 ++ 6 files changed, 117 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35c58b7539..3a4282358f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,8 +25,8 @@ All notable changes to this project will be documented in this file. Take a look ### Changed * Readium resources are now prefixed with `readium_`. Take care of updating any overridden resource by following [the migration guide](docs/migration-guide.md#300). -* `Link` and `Locator`'s `href` do not start with a `/` for packaged publications anymore. - * To ensure backward-compatibility, `href` starting with a `/` are still supported. But you may want to update the locators persisted in your database to drop the `/` prefix for packaged publications. +* `Link` and `Locator`'s `href` are normalized as valid URLs to improve interoperability with the Readium Web toolkits. + * **You MUST migrate your database if you were persisting HREFs and Locators**. Take a look at [the migration guide](docs/migration-guide.md) for guidance. #### Shared diff --git a/docs/migration-guide.md b/docs/migration-guide.md index a6fb823fd3..ec4d56261c 100644 --- a/docs/migration-guide.md +++ b/docs/migration-guide.md @@ -29,6 +29,55 @@ dependencies { } ``` +### Migration of HREFs and Locators (bookmarks, annotations, etc.) + +:warning: This requires a database migration in your application, if you were persisting `Locator` objects. + +In Readium v2.x, a `Link` or `Locator`'s `href` could be either: + +* a valid absolute URL for a streamed publication, e.g. `https://domain.com/isbn/dir/my%20chapter.html`, +* a percent-decoded path for a local archive such as an EPUB, e.g. `/dir/my chapter.html`. + * Note that it was relative to the root of the archive (`/`). + +To improve the interoperability with other Readium toolkits (in particular the Readium Web Toolkits, which only work in a streaming context) **Readium v3 now generates and expects valid URLs** for `Locator` and `Link`'s `href`. + +* `https://domain.com/isbn/dir/my%20chapter.html` is left unchanged, as it was already a valid URL. +* `/dir/my chapter.html` becomes the relative URL path `dir/my%20chapter.html` + * We dropped the `/` prefix to avoid issues when resolving to a base URL. + * Special characters are percent-encoded. + +**You must migrate the HREFs or Locators stored in your database** when upgrading to Readium 3. To assist you, two helpers are provided: `Url.fromLegacyHref()` and `Locator.fromLegacyJSON()`. + +Here's an example of a Jetpack Room migration that can serve as inspiration: + +```kotlin +val MIGRATION_HREF = object : Migration(1, 2) { + + override fun migrate(db: SupportSQLiteDatabase) { + val normalizedHrefs: Map = buildMap { + db.query("SELECT id, href FROM bookmarks").use { cursor -> + while (cursor.moveToNext()) { + val id = cursor.getLong(0) + val href = cursor.getString(1) + + val normalizedHref = Url.fromLegacyHref(href)?.toString() + if (normalizedHref != null) { + put(id, normalizedHref) + } + } + } + } + + val stmt = db.compileStatement("UPDATE bookmarks SET href = ? WHERE id = ?") + for ((id, href) in normalizedHrefs) { + stmt.bindString(1, href) + stmt.bindLong(2, id) + stmt.executeUpdateDelete() + } + } +} +``` + ### All resources now have the prefix `readium_`. To avoid conflicts when merging your app resources, all resources declared in the Readium toolkit now have the prefix `readium_`. This means that you must rename any layouts or strings you have overridden. Here is a comprehensive list of the changes. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt index 556f182db1..aabe739d4b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Locator.kt @@ -14,6 +14,7 @@ import kotlinx.parcelize.Parcelize import kotlinx.parcelize.WriteWith import org.json.JSONArray import org.json.JSONObject +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.JSONable import org.readium.r2.shared.extensions.* import org.readium.r2.shared.toJSON @@ -197,10 +198,36 @@ public data class Locator( public companion object { + /** + * Creates a [Locator] from its JSON representation. + */ public fun fromJSON( json: JSONObject?, mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null + ): Locator? = + fromJSON(json, mediaTypeRetriever, warnings, withLegacyHref = false) + + /** + * Creates a [Locator] from its legacy JSON representation. + * + * Only use this API when you are upgrading to Readium 3.x and migrating the [Locator] + * objects stored in your database. See the migration guide for more information. + */ + @DelicateReadiumApi + public fun fromLegacyJSON( + json: JSONObject?, + mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), + warnings: WarningLogger? = null + ): Locator? = + fromJSON(json, mediaTypeRetriever, warnings, withLegacyHref = true) + + @OptIn(DelicateReadiumApi::class) + private fun fromJSON( + json: JSONObject?, + mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), + warnings: WarningLogger? = null, + withLegacyHref: Boolean = false ): Locator? { val href = json?.optNullableString("href") val type = json?.optNullableString("type") @@ -209,7 +236,10 @@ public data class Locator( return null } - val url = Url(href) ?: run { + val url = ( + if (withLegacyHref) Url.fromLegacyHref(href) + else Url(href) + ) ?: run { warnings?.log(Locator::class.java, "[href] is not a valid URL", json) return null } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt index 7aace6a3dc..bd67a5960a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt @@ -13,6 +13,7 @@ import java.io.File import java.net.URI import java.net.URL import kotlinx.parcelize.Parcelize +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.extensions.percentEncodedPath import org.readium.r2.shared.extensions.tryOrNull @@ -34,9 +35,6 @@ public sealed class Url : Parcelable { public fun fromDecodedPath(path: String): RelativeUrl? = RelativeUrl(path.percentEncodedPath()) - public fun fromLegacyHref(href: String): Url? = - AbsoluteUrl(href) ?: fromDecodedPath(href.removePrefix("/")) - /** * Creates a [Url] from its encoded string representation. */ @@ -45,6 +43,19 @@ public sealed class Url : Parcelable { return invoke(Uri.parse(url)) } + /** + * Creates an [Url] from a legacy HREF. + * + * For example, if it is a relative path such as `/dir/my chapter.html`, it will be + * converted to the valid relative URL `dir/my%20chapter.html`. + * + * Only use this API when you are upgrading to Readium 3.x and migrating the HREFs stored in + * your database. See the 3.0 migration guide for more information. + */ + @DelicateReadiumApi + public fun fromLegacyHref(href: String): Url? = + AbsoluteUrl(href) ?: fromDecodedPath(href.removePrefix("/")) + internal operator fun invoke(uri: Uri): Url? = if (uri.isAbsolute) { AbsoluteUrl(uri) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt index 846905d0eb..f74c01a6b8 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt @@ -14,6 +14,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.assertJSONEquals import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType @@ -71,6 +72,23 @@ class LocatorTest { assertNull(Locator.fromJSON(JSONObject("{ 'invalid': 'object' }"))) } + @OptIn(DelicateReadiumApi::class) + @Test fun `parse {Locator} with legacy HREF`() { + val json = JSONObject(""" + { + "href": "legacy href", + "type": "text/html" + } + """) + + assertNull(Locator.fromJSON(json)) + + assertEquals( + Locator(href = Url("legacy%20href")!!, mediaType = MediaType.HTML), + Locator.fromLegacyJSON(json) + ) + } + @Test fun `get {Locator} minimal JSON`() { assertJSONEquals( JSONObject( diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/UrlTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/UrlTest.kt index 59a8fd30db..346dcddaae 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/UrlTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/UrlTest.kt @@ -10,6 +10,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.util.Url.Query import org.readium.r2.shared.util.Url.QueryParameter import org.robolectric.RobolectricTestRunner @@ -46,6 +47,7 @@ class UrlTest { assertEquals(null, url.fragment) } + @OptIn(DelicateReadiumApi::class) @Test fun createFromLegacyHref() { testLegacy("dir/chapter.xhtml", "dir/chapter.xhtml") @@ -65,6 +67,7 @@ class UrlTest { assertNull(Url.fromLegacyHref("http://domain.com/a book")) } + @OptIn(DelicateReadiumApi::class) private inline fun testLegacy(href: String, expected: String) { val url = Url.fromLegacyHref(href) assertNotNull(url)