diff --git a/CHANGELOG.md b/CHANGELOG.md index 35c58b7539..466c172b57 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 HREF 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..e76397d074 100644 --- a/docs/migration-guide.md +++ b/docs/migration-guide.md @@ -29,6 +29,62 @@ dependencies { } ``` +### Migration of HREFs (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 full valid 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 already a valid URL. +* `/dir/my chapter.html` becomes the relative URL path `dir/my%20chapter.html` + * We dropped the `/` prefix to avoid issues with 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 in migrating your data, two helpers are provided: `Url.fromLegacyHref()` and `Locator.fromJSON(withLegacyHref)`: + +```kotlin +val href = Url.fromLegacyHref(legacyHref)?.toString() +val locator = Locator.fromJSON(legacyJSON, withLegacyHref = true) +``` + +Here's a sample Jetpack Room migration that you can use 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..7bf5325930 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 @@ -200,7 +200,8 @@ public data class Locator( public fun fromJSON( json: JSONObject?, mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), - warnings: WarningLogger? = null + warnings: WarningLogger? = null, + withLegacyHref: Boolean = false ): Locator? { val href = json?.optNullableString("href") val type = json?.optNullableString("type") @@ -209,7 +210,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/test/java/org/readium/r2/shared/publication/LocatorTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt index 846905d0eb..e197f3daf8 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 @@ -71,6 +71,23 @@ class LocatorTest { assertNull(Locator.fromJSON(JSONObject("{ 'invalid': 'object' }"))) } + @Test fun `parse {Locator} with legacy HREF`() { + val json = JSONObject(""" + { + "href": "legacy href", + "type": "text/html" + } + """) + + assertNull(Locator.fromJSON(json)) + assertNull(Locator.fromJSON(json, withLegacyHref = false)) + + assertEquals( + Locator(href = Url("legacy%20href")!!, mediaType = MediaType.HTML), + Locator.fromJSON(json, withLegacyHref = true) + ) + } + @Test fun `get {Locator} minimal JSON`() { assertJSONEquals( JSONObject(