Skip to content

Commit

Permalink
Refactor HREF normalization and models (#387)
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-menu authored Sep 21, 2023
1 parent e09da40 commit 482ab0c
Show file tree
Hide file tree
Showing 194 changed files with 3,368 additions and 2,946 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +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.

#### Shared

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.util.getOrThrow
import org.readium.r2.shared.util.pdf.PdfDocument
import org.readium.r2.shared.util.pdf.PdfDocumentFactory
import org.readium.r2.shared.util.toFile
import org.readium.r2.shared.util.use
import timber.log.Timber

Expand Down
103 changes: 55 additions & 48 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,11 @@ import org.readium.r2.shared.publication.encryption.encryption
import org.readium.r2.shared.publication.flatten
import org.readium.r2.shared.publication.protection.ContentProtection
import org.readium.r2.shared.publication.services.contentProtectionServiceFactory
import org.readium.r2.shared.resource.ArchiveFactory
import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.resource.ResourceFactory
import org.readium.r2.shared.resource.TransformingContainer
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.ThrowableError
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.flatMap
import org.readium.r2.shared.util.getOrElse

Expand All @@ -45,7 +43,7 @@ internal class LcpContentProtection(
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<ContentProtection.Asset, Publication.OpeningException> {
): Try<ContentProtection.Asset, Publication.OpenError> {
return when (asset) {
is Asset.Container -> openPublication(asset, credentials, allowUserInteraction, sender)
is Asset.Resource -> openLicense(asset, credentials, allowUserInteraction, sender)
Expand All @@ -57,7 +55,7 @@ internal class LcpContentProtection(
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<ContentProtection.Asset, Publication.OpeningException> {
): Try<ContentProtection.Asset, Publication.OpenError> {
val license = retrieveLicense(asset, credentials, allowUserInteraction, sender)
return createResultAsset(asset, license)
}
Expand All @@ -78,7 +76,7 @@ internal class LcpContentProtection(
private fun createResultAsset(
asset: Asset.Container,
license: Try<LcpLicense, LcpException>
): Try<ContentProtection.Asset, Publication.OpeningException> {
): Try<ContentProtection.Asset, Publication.OpenError> {
val serviceFactory = LcpContentProtectionService
.createFactory(license.getOrNull(), license.failureOrNull())

Expand All @@ -92,7 +90,9 @@ internal class LcpContentProtection(
onCreatePublication = {
decryptor.encryptionData = (manifest.readingOrder + manifest.resources + manifest.links)
.flatten()
.mapNotNull { it.properties.encryption?.let { enc -> it.href to enc } }
.mapNotNull {
it.properties.encryption?.let { enc -> it.url() to enc }
}
.toMap()

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

val licenseDoc = license.getOrNull()?.license
Expand All @@ -117,9 +117,7 @@ internal class LcpContentProtection(
LicenseDocument(it)
} catch (e: Exception) {
return Try.failure(
Publication.OpeningException.ParsingFailed(
ThrowableError(e)
)
Publication.OpenError.InvalidAsset(cause = ThrowableError(e))
)
}
}
Expand All @@ -129,56 +127,65 @@ internal class LcpContentProtection(
)
}

val link = checkNotNull(licenseDoc.link(LicenseDocument.Rel.Publication))
val url = Url(link.url.toString())
val link = licenseDoc.publicationLink
val url = (link.url() as? AbsoluteUrl)
?: return Try.failure(
Publication.OpeningException.ParsingFailed(
ThrowableError(
Publication.OpenError.InvalidAsset(
cause = ThrowableError(
LcpException.Parsing.Url(rel = LicenseDocument.Rel.Publication.value)
)
)
)

return assetRetriever.retrieve(
url,
mediaType = link.mediaType,
assetType = AssetType.Archive
)
.mapFailure { Publication.OpeningException.ParsingFailed(it) }
.flatMap { createResultAsset(it as Asset.Container, license) }
}

private fun ResourceFactory.Error.wrap(): Publication.OpeningException =
when (this) {
is ResourceFactory.Error.NotAResource ->
Publication.OpeningException.NotFound()
is ResourceFactory.Error.Forbidden ->
Publication.OpeningException.Forbidden()
is ResourceFactory.Error.SchemeNotSupported ->
Publication.OpeningException.UnsupportedAsset()
}
val asset =
if (link.mediaType != null) {
assetRetriever.retrieve(
url,
mediaType = link.mediaType,
assetType = AssetType.Archive
)
.map { it as Asset.Container }
.mapFailure { it.wrap() }
} else {
(assetRetriever.retrieve(url) as? Asset.Container)
?.let { Try.success(it) }
?: Try.failure(Publication.OpenError.UnsupportedAsset())
}

private fun ArchiveFactory.Error.wrap(): Publication.OpeningException =
when (this) {
is ArchiveFactory.Error.FormatNotSupported ->
Publication.OpeningException.UnsupportedAsset()
is ArchiveFactory.Error.PasswordsNotSupported ->
Publication.OpeningException.UnsupportedAsset()
is ArchiveFactory.Error.ResourceReading ->
resourceException.wrap()
}
return asset.flatMap { createResultAsset(it, license) }
}

private fun Resource.Exception.wrap(): Publication.OpeningException =
private fun Resource.Exception.wrap(): Publication.OpenError =
when (this) {
is Resource.Exception.Forbidden ->
Publication.OpeningException.Forbidden(ThrowableError(this))
Publication.OpenError.Forbidden(ThrowableError(this))
is Resource.Exception.NotFound ->
Publication.OpeningException.NotFound(ThrowableError(this))
Publication.OpenError.NotFound(ThrowableError(this))
Resource.Exception.Offline, is Resource.Exception.Unavailable ->
Publication.OpeningException.Unavailable(ThrowableError(this))
Publication.OpenError.Unavailable(ThrowableError(this))
is Resource.Exception.Other, is Resource.Exception.BadRequest ->
Publication.OpeningException.Unexpected(this)
Publication.OpenError.Unknown(this)
is Resource.Exception.OutOfMemory ->
Publication.OpeningException.OutOfMemory(ThrowableError(this))
Publication.OpenError.OutOfMemory(ThrowableError(this))
}

private fun AssetRetriever.Error.wrap(): Publication.OpenError =
when (this) {
is AssetRetriever.Error.ArchiveFormatNotSupported ->
Publication.OpenError.UnsupportedAsset(this)
is AssetRetriever.Error.Forbidden ->
Publication.OpenError.Forbidden(this)
is AssetRetriever.Error.InvalidAsset ->
Publication.OpenError.InvalidAsset(this)
is AssetRetriever.Error.NotFound ->
Publication.OpenError.NotFound(this)
is AssetRetriever.Error.OutOfMemory ->
Publication.OpenError.OutOfMemory(this)
is AssetRetriever.Error.SchemeNotSupported ->
Publication.OpenError.UnsupportedAsset(this)
is AssetRetriever.Error.Unavailable ->
Publication.OpenError.Unavailable(this)
is AssetRetriever.Error.Unknown ->
Publication.OpenError.Unknown(this)
}
}
7 changes: 4 additions & 3 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.readium.r2.shared.resource.TransformingResource
import org.readium.r2.shared.resource.flatMap
import org.readium.r2.shared.resource.flatMapCatching
import org.readium.r2.shared.resource.mapCatching
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.getOrElse
Expand All @@ -32,7 +33,7 @@ import org.readium.r2.shared.util.getOrThrow
*/
internal class LcpDecryptor(
val license: LcpLicense?,
var encryptionData: Map<String, Encryption> = emptyMap()
var encryptionData: Map<Url, Encryption> = emptyMap()
) {

fun transform(resource: Resource): Resource {
Expand All @@ -41,7 +42,7 @@ internal class LcpDecryptor(
}

return resource.flatMap {
val encryption = encryptionData[resource.path]
val encryption = encryptionData[resource.url]

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

override val source: Url? = null
override val source: AbsoluteUrl? = null

private class Cache(
var startIndex: Int? = null,
Expand Down
7 changes: 4 additions & 3 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.annotation.StringRes
import java.net.SocketTimeoutException
import java.util.*
import org.readium.r2.shared.UserException
import org.readium.r2.shared.util.Url

public sealed class LcpException(
userMessageId: Int,
Expand Down Expand Up @@ -203,17 +204,17 @@ public sealed class LcpException(
public object OpenFailed : Container(R.string.readium_lcp_exception_container_open_failed)

/** The file at given relative path is not found in the Container. */
public class FileNotFound(public val path: String) : Container(
public class FileNotFound(public val url: Url) : Container(
R.string.readium_lcp_exception_container_file_not_found
)

/** Can't read the file at given relative path in the Container. */
public class ReadFailed(public val path: String) : Container(
public class ReadFailed(public val url: Url?) : Container(
R.string.readium_lcp_exception_container_read_failed
)

/** Can't write the file at given relative path in the Container. */
public class WriteFailed(public val path: String) : Container(
public class WriteFailed(public val url: Url?) : Container(
R.string.readium_lcp_exception_container_write_failed
)
}
Expand Down
3 changes: 2 additions & 1 deletion readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.lcp.license.model.StatusDocument
import org.readium.r2.shared.publication.services.ContentProtectionService
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.Url
import timber.log.Timber

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

@Deprecated(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import kotlinx.coroutines.launch
import org.readium.r2.lcp.license.container.createLicenseContainer
import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.shared.extensions.tryOrLog
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.downloads.DownloadManager
import org.readium.r2.shared.util.mediatype.FormatRegistry
import org.readium.r2.shared.util.mediatype.MediaType
import org.readium.r2.shared.util.mediatype.MediaTypeHints
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever

/**
Expand Down Expand Up @@ -152,7 +152,7 @@ public class LcpPublicationRetriever(
private fun fetchPublication(
license: LicenseDocument
): RequestId {
val url = Url(license.publicationLink.url)
val url = license.publicationLink.url()

val requestId = downloadManager.submit(
request = DownloadManager.Request(
Expand Down Expand Up @@ -192,9 +192,11 @@ public class LcpPublicationRetriever(
downloadsRepository.removeDownload(requestId.value)

val mt = mediaTypeRetriever.retrieve(
mediaTypes = listOfNotNull(
license.publicationLink.type,
download.mediaType.toString()
MediaTypeHints(
mediaTypes = listOfNotNull(
license.publicationLink.mediaType,
download.mediaType
)
)
) ?: MediaType.EPUB

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.browser.customtabs.CustomTabsIntent
import androidx.fragment.app.FragmentManager
import com.google.android.material.datepicker.*
import java.net.URL
import java.util.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.suspendCancellableCoroutine
import org.readium.r2.shared.util.Url

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

override suspend fun openWebPage(url: URL) {
override suspend fun openWebPage(url: Url) {
suspendCoroutine { cont ->
webPageContinuation = cont

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import org.readium.r2.lcp.R
import org.readium.r2.lcp.license.model.components.Link
import org.readium.r2.shared.extensions.tryOr
import org.readium.r2.shared.extensions.tryOrNull
import org.readium.r2.shared.util.AbsoluteUrl
import org.readium.r2.shared.util.toUri
import timber.log.Timber

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

private fun Context.startActivityForLink(link: Link) {
val url = tryOrNull { Uri.parse(link.href) } ?: return
val url = tryOrNull { (link.url() as? AbsoluteUrl) } ?: return

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

startActivity(
Intent(action).apply {
data = url
data = url.toUri()
}
)
}
Expand Down
Loading

0 comments on commit 482ab0c

Please sign in to comment.