Skip to content

Commit 482ab0c

Browse files
authored
Refactor HREF normalization and models (#387)
1 parent e09da40 commit 482ab0c

File tree

194 files changed

+3368
-2946
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

194 files changed

+3368
-2946
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ All notable changes to this project will be documented in this file. Take a look
2525
### Changed
2626

2727
* Readium resources are now prefixed with `readium_`. Take care of updating any overridden resource by following [the migration guide](docs/migration-guide.md#300).
28+
* `Link` and `Locator`'s `href` do not start with a `/` for packaged publications anymore.
29+
* 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.
2830

2931
#### Shared
3032

readium/adapters/pdfium/pdfium-document/src/main/java/org/readium/adapters/pdfium/document/PdfiumDocument.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import org.readium.r2.shared.resource.Resource
2222
import org.readium.r2.shared.util.getOrThrow
2323
import org.readium.r2.shared.util.pdf.PdfDocument
2424
import org.readium.r2.shared.util.pdf.PdfDocumentFactory
25-
import org.readium.r2.shared.util.toFile
2625
import org.readium.r2.shared.util.use
2726
import timber.log.Timber
2827

readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt

Lines changed: 55 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,11 @@ import org.readium.r2.shared.publication.encryption.encryption
1616
import org.readium.r2.shared.publication.flatten
1717
import org.readium.r2.shared.publication.protection.ContentProtection
1818
import org.readium.r2.shared.publication.services.contentProtectionServiceFactory
19-
import org.readium.r2.shared.resource.ArchiveFactory
2019
import org.readium.r2.shared.resource.Resource
21-
import org.readium.r2.shared.resource.ResourceFactory
2220
import org.readium.r2.shared.resource.TransformingContainer
21+
import org.readium.r2.shared.util.AbsoluteUrl
2322
import org.readium.r2.shared.util.ThrowableError
2423
import org.readium.r2.shared.util.Try
25-
import org.readium.r2.shared.util.Url
2624
import org.readium.r2.shared.util.flatMap
2725
import org.readium.r2.shared.util.getOrElse
2826

@@ -45,7 +43,7 @@ internal class LcpContentProtection(
4543
credentials: String?,
4644
allowUserInteraction: Boolean,
4745
sender: Any?
48-
): Try<ContentProtection.Asset, Publication.OpeningException> {
46+
): Try<ContentProtection.Asset, Publication.OpenError> {
4947
return when (asset) {
5048
is Asset.Container -> openPublication(asset, credentials, allowUserInteraction, sender)
5149
is Asset.Resource -> openLicense(asset, credentials, allowUserInteraction, sender)
@@ -57,7 +55,7 @@ internal class LcpContentProtection(
5755
credentials: String?,
5856
allowUserInteraction: Boolean,
5957
sender: Any?
60-
): Try<ContentProtection.Asset, Publication.OpeningException> {
58+
): Try<ContentProtection.Asset, Publication.OpenError> {
6159
val license = retrieveLicense(asset, credentials, allowUserInteraction, sender)
6260
return createResultAsset(asset, license)
6361
}
@@ -78,7 +76,7 @@ internal class LcpContentProtection(
7876
private fun createResultAsset(
7977
asset: Asset.Container,
8078
license: Try<LcpLicense, LcpException>
81-
): Try<ContentProtection.Asset, Publication.OpeningException> {
79+
): Try<ContentProtection.Asset, Publication.OpenError> {
8280
val serviceFactory = LcpContentProtectionService
8381
.createFactory(license.getOrNull(), license.failureOrNull())
8482

@@ -92,7 +90,9 @@ internal class LcpContentProtection(
9290
onCreatePublication = {
9391
decryptor.encryptionData = (manifest.readingOrder + manifest.resources + manifest.links)
9492
.flatten()
95-
.mapNotNull { it.properties.encryption?.let { enc -> it.href to enc } }
93+
.mapNotNull {
94+
it.properties.encryption?.let { enc -> it.url() to enc }
95+
}
9696
.toMap()
9797

9898
servicesBuilder.contentProtectionServiceFactory = serviceFactory
@@ -107,7 +107,7 @@ internal class LcpContentProtection(
107107
credentials: String?,
108108
allowUserInteraction: Boolean,
109109
sender: Any?
110-
): Try<ContentProtection.Asset, Publication.OpeningException> {
110+
): Try<ContentProtection.Asset, Publication.OpenError> {
111111
val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction, sender)
112112

113113
val licenseDoc = license.getOrNull()?.license
@@ -117,9 +117,7 @@ internal class LcpContentProtection(
117117
LicenseDocument(it)
118118
} catch (e: Exception) {
119119
return Try.failure(
120-
Publication.OpeningException.ParsingFailed(
121-
ThrowableError(e)
122-
)
120+
Publication.OpenError.InvalidAsset(cause = ThrowableError(e))
123121
)
124122
}
125123
}
@@ -129,56 +127,65 @@ internal class LcpContentProtection(
129127
)
130128
}
131129

132-
val link = checkNotNull(licenseDoc.link(LicenseDocument.Rel.Publication))
133-
val url = Url(link.url.toString())
130+
val link = licenseDoc.publicationLink
131+
val url = (link.url() as? AbsoluteUrl)
134132
?: return Try.failure(
135-
Publication.OpeningException.ParsingFailed(
136-
ThrowableError(
133+
Publication.OpenError.InvalidAsset(
134+
cause = ThrowableError(
137135
LcpException.Parsing.Url(rel = LicenseDocument.Rel.Publication.value)
138136
)
139137
)
140138
)
141139

142-
return assetRetriever.retrieve(
143-
url,
144-
mediaType = link.mediaType,
145-
assetType = AssetType.Archive
146-
)
147-
.mapFailure { Publication.OpeningException.ParsingFailed(it) }
148-
.flatMap { createResultAsset(it as Asset.Container, license) }
149-
}
150-
151-
private fun ResourceFactory.Error.wrap(): Publication.OpeningException =
152-
when (this) {
153-
is ResourceFactory.Error.NotAResource ->
154-
Publication.OpeningException.NotFound()
155-
is ResourceFactory.Error.Forbidden ->
156-
Publication.OpeningException.Forbidden()
157-
is ResourceFactory.Error.SchemeNotSupported ->
158-
Publication.OpeningException.UnsupportedAsset()
159-
}
140+
val asset =
141+
if (link.mediaType != null) {
142+
assetRetriever.retrieve(
143+
url,
144+
mediaType = link.mediaType,
145+
assetType = AssetType.Archive
146+
)
147+
.map { it as Asset.Container }
148+
.mapFailure { it.wrap() }
149+
} else {
150+
(assetRetriever.retrieve(url) as? Asset.Container)
151+
?.let { Try.success(it) }
152+
?: Try.failure(Publication.OpenError.UnsupportedAsset())
153+
}
160154

161-
private fun ArchiveFactory.Error.wrap(): Publication.OpeningException =
162-
when (this) {
163-
is ArchiveFactory.Error.FormatNotSupported ->
164-
Publication.OpeningException.UnsupportedAsset()
165-
is ArchiveFactory.Error.PasswordsNotSupported ->
166-
Publication.OpeningException.UnsupportedAsset()
167-
is ArchiveFactory.Error.ResourceReading ->
168-
resourceException.wrap()
169-
}
155+
return asset.flatMap { createResultAsset(it, license) }
156+
}
170157

171-
private fun Resource.Exception.wrap(): Publication.OpeningException =
158+
private fun Resource.Exception.wrap(): Publication.OpenError =
172159
when (this) {
173160
is Resource.Exception.Forbidden ->
174-
Publication.OpeningException.Forbidden(ThrowableError(this))
161+
Publication.OpenError.Forbidden(ThrowableError(this))
175162
is Resource.Exception.NotFound ->
176-
Publication.OpeningException.NotFound(ThrowableError(this))
163+
Publication.OpenError.NotFound(ThrowableError(this))
177164
Resource.Exception.Offline, is Resource.Exception.Unavailable ->
178-
Publication.OpeningException.Unavailable(ThrowableError(this))
165+
Publication.OpenError.Unavailable(ThrowableError(this))
179166
is Resource.Exception.Other, is Resource.Exception.BadRequest ->
180-
Publication.OpeningException.Unexpected(this)
167+
Publication.OpenError.Unknown(this)
181168
is Resource.Exception.OutOfMemory ->
182-
Publication.OpeningException.OutOfMemory(ThrowableError(this))
169+
Publication.OpenError.OutOfMemory(ThrowableError(this))
170+
}
171+
172+
private fun AssetRetriever.Error.wrap(): Publication.OpenError =
173+
when (this) {
174+
is AssetRetriever.Error.ArchiveFormatNotSupported ->
175+
Publication.OpenError.UnsupportedAsset(this)
176+
is AssetRetriever.Error.Forbidden ->
177+
Publication.OpenError.Forbidden(this)
178+
is AssetRetriever.Error.InvalidAsset ->
179+
Publication.OpenError.InvalidAsset(this)
180+
is AssetRetriever.Error.NotFound ->
181+
Publication.OpenError.NotFound(this)
182+
is AssetRetriever.Error.OutOfMemory ->
183+
Publication.OpenError.OutOfMemory(this)
184+
is AssetRetriever.Error.SchemeNotSupported ->
185+
Publication.OpenError.UnsupportedAsset(this)
186+
is AssetRetriever.Error.Unavailable ->
187+
Publication.OpenError.Unavailable(this)
188+
is AssetRetriever.Error.Unknown ->
189+
Publication.OpenError.Unknown(this)
183190
}
184191
}

readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import org.readium.r2.shared.resource.TransformingResource
2222
import org.readium.r2.shared.resource.flatMap
2323
import org.readium.r2.shared.resource.flatMapCatching
2424
import org.readium.r2.shared.resource.mapCatching
25+
import org.readium.r2.shared.util.AbsoluteUrl
2526
import org.readium.r2.shared.util.Try
2627
import org.readium.r2.shared.util.Url
2728
import org.readium.r2.shared.util.getOrElse
@@ -32,7 +33,7 @@ import org.readium.r2.shared.util.getOrThrow
3233
*/
3334
internal class LcpDecryptor(
3435
val license: LcpLicense?,
35-
var encryptionData: Map<String, Encryption> = emptyMap()
36+
var encryptionData: Map<Url, Encryption> = emptyMap()
3637
) {
3738

3839
fun transform(resource: Resource): Resource {
@@ -41,7 +42,7 @@ internal class LcpDecryptor(
4142
}
4243

4344
return resource.flatMap {
44-
val encryption = encryptionData[resource.path]
45+
val encryption = encryptionData[resource.url]
4546

4647
// Checks if the resource is encrypted and whether the encryption schemes of the resource
4748
// and the DRM license are the same.
@@ -93,7 +94,7 @@ internal class LcpDecryptor(
9394
private val license: LcpLicense
9495
) : Resource by resource {
9596

96-
override val source: Url? = null
97+
override val source: AbsoluteUrl? = null
9798

9899
private class Cache(
99100
var startIndex: Int? = null,

readium/lcp/src/main/java/org/readium/r2/lcp/LcpException.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.annotation.StringRes
1111
import java.net.SocketTimeoutException
1212
import java.util.*
1313
import org.readium.r2.shared.UserException
14+
import org.readium.r2.shared.util.Url
1415

1516
public sealed class LcpException(
1617
userMessageId: Int,
@@ -203,17 +204,17 @@ public sealed class LcpException(
203204
public object OpenFailed : Container(R.string.readium_lcp_exception_container_open_failed)
204205

205206
/** The file at given relative path is not found in the Container. */
206-
public class FileNotFound(public val path: String) : Container(
207+
public class FileNotFound(public val url: Url) : Container(
207208
R.string.readium_lcp_exception_container_file_not_found
208209
)
209210

210211
/** Can't read the file at given relative path in the Container. */
211-
public class ReadFailed(public val path: String) : Container(
212+
public class ReadFailed(public val url: Url?) : Container(
212213
R.string.readium_lcp_exception_container_read_failed
213214
)
214215

215216
/** Can't write the file at given relative path in the Container. */
216-
public class WriteFailed(public val path: String) : Container(
217+
public class WriteFailed(public val url: Url?) : Container(
217218
R.string.readium_lcp_exception_container_write_failed
218219
)
219220
}

readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import org.readium.r2.lcp.license.model.LicenseDocument
1717
import org.readium.r2.lcp.license.model.StatusDocument
1818
import org.readium.r2.shared.publication.services.ContentProtectionService
1919
import org.readium.r2.shared.util.Try
20+
import org.readium.r2.shared.util.Url
2021
import timber.log.Timber
2122

2223
/**
@@ -102,7 +103,7 @@ public interface LcpLicense : ContentProtectionService.UserRights {
102103
* You should present the URL in a Chrome Custom Tab and terminate the function when the
103104
* web page is dismissed by the user.
104105
*/
105-
public suspend fun openWebPage(url: URL)
106+
public suspend fun openWebPage(url: Url)
106107
}
107108

108109
@Deprecated(

readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import kotlinx.coroutines.launch
1313
import org.readium.r2.lcp.license.container.createLicenseContainer
1414
import org.readium.r2.lcp.license.model.LicenseDocument
1515
import org.readium.r2.shared.extensions.tryOrLog
16-
import org.readium.r2.shared.util.Url
1716
import org.readium.r2.shared.util.downloads.DownloadManager
1817
import org.readium.r2.shared.util.mediatype.FormatRegistry
1918
import org.readium.r2.shared.util.mediatype.MediaType
19+
import org.readium.r2.shared.util.mediatype.MediaTypeHints
2020
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever
2121

2222
/**
@@ -152,7 +152,7 @@ public class LcpPublicationRetriever(
152152
private fun fetchPublication(
153153
license: LicenseDocument
154154
): RequestId {
155-
val url = Url(license.publicationLink.url)
155+
val url = license.publicationLink.url()
156156

157157
val requestId = downloadManager.submit(
158158
request = DownloadManager.Request(
@@ -192,9 +192,11 @@ public class LcpPublicationRetriever(
192192
downloadsRepository.removeDownload(requestId.value)
193193

194194
val mt = mediaTypeRetriever.retrieve(
195-
mediaTypes = listOfNotNull(
196-
license.publicationLink.type,
197-
download.mediaType.toString()
195+
MediaTypeHints(
196+
mediaTypes = listOfNotNull(
197+
license.publicationLink.mediaType,
198+
download.mediaType
199+
)
198200
)
199201
) ?: MediaType.EPUB
200202

readium/lcp/src/main/java/org/readium/r2/lcp/MaterialRenewListener.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ import androidx.activity.result.contract.ActivityResultContracts
1212
import androidx.browser.customtabs.CustomTabsIntent
1313
import androidx.fragment.app.FragmentManager
1414
import com.google.android.material.datepicker.*
15-
import java.net.URL
1615
import java.util.*
1716
import kotlin.coroutines.Continuation
1817
import kotlin.coroutines.resume
1918
import kotlin.coroutines.suspendCoroutine
2019
import kotlinx.coroutines.suspendCancellableCoroutine
20+
import org.readium.r2.shared.util.Url
2121

2222
/**
2323
* A default implementation of the [LcpLicense.RenewListener] using Chrome Custom Tabs for
@@ -73,7 +73,7 @@ public class MaterialRenewListener(
7373
.show(fragmentManager, "MaterialRenewListener.DatePicker")
7474
}
7575

76-
override suspend fun openWebPage(url: URL) {
76+
override suspend fun openWebPage(url: Url) {
7777
suspendCoroutine { cont ->
7878
webPageContinuation = cont
7979

readium/lcp/src/main/java/org/readium/r2/lcp/auth/LcpDialogAuthentication.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import org.readium.r2.lcp.R
3434
import org.readium.r2.lcp.license.model.components.Link
3535
import org.readium.r2.shared.extensions.tryOr
3636
import org.readium.r2.shared.extensions.tryOrNull
37+
import org.readium.r2.shared.util.AbsoluteUrl
38+
import org.readium.r2.shared.util.toUri
3739
import timber.log.Timber
3840

3941
/**
@@ -152,7 +154,7 @@ public class LcpDialogAuthentication : LcpAuthenticating {
152154
private fun showHelpDialog(context: Context, links: List<Link>) {
153155
val titles = links.map {
154156
it.title ?: tryOr(context.getString(R.string.readium_lcp_dialog_support)) {
155-
when (Uri.parse(it.href).scheme) {
157+
when ((it.url() as? AbsoluteUrl)?.scheme?.value) {
156158
"http", "https" -> context.getString(R.string.readium_lcp_dialog_support_web)
157159
"tel" -> context.getString(R.string.readium_lcp_dialog_support_phone)
158160
"mailto" -> context.getString(R.string.readium_lcp_dialog_support_mail)
@@ -169,9 +171,9 @@ public class LcpDialogAuthentication : LcpAuthenticating {
169171
}
170172

171173
private fun Context.startActivityForLink(link: Link) {
172-
val url = tryOrNull { Uri.parse(link.href) } ?: return
174+
val url = tryOrNull { (link.url() as? AbsoluteUrl) } ?: return
173175

174-
val action = when (url.scheme?.lowercase(Locale.ROOT)) {
176+
val action = when (url.scheme.value) {
175177
"http", "https" -> Intent(Intent.ACTION_VIEW)
176178
"tel" -> Intent(Intent.ACTION_CALL)
177179
"mailto" -> Intent(Intent.ACTION_SEND)
@@ -180,7 +182,7 @@ public class LcpDialogAuthentication : LcpAuthenticating {
180182

181183
startActivity(
182184
Intent(action).apply {
183-
data = url
185+
data = url.toUri()
184186
}
185187
)
186188
}

0 commit comments

Comments
 (0)