Skip to content

Refactor Url and Href #388

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
20b5965
Refactor `Url` to support relative URLs
mickael-menu Aug 31, 2023
57d0dcd
Use `Url` and `Href` everywhere
mickael-menu Sep 1, 2023
faf8bd7
Move `Href`
mickael-menu Sep 1, 2023
1360072
Normalize the HREFs of an OPDS 2 manifest
mickael-menu Sep 1, 2023
d7540ed
Normalize remote web publication locators
mickael-menu Sep 1, 2023
3d3d023
Minor fixes
mickael-menu Sep 1, 2023
39ca985
Use `MediaType` in `Locator` instead of a string
mickael-menu Sep 1, 2023
433a430
Ergonomics
mickael-menu Sep 3, 2023
0a661ee
Refactor LCP `Link`
mickael-menu Sep 3, 2023
722283c
Fix regression
mickael-menu Sep 4, 2023
6945501
Add guards to crash when attempting to compare an `Url` to an `Href`
mickael-menu Sep 4, 2023
abc816b
Various changes
mickael-menu Sep 6, 2023
41987a6
Fix regression parsing an EPUB table of contents
mickael-menu Sep 7, 2023
d8bbdd6
Prevent creating `Url` with invalid characters
mickael-menu Sep 11, 2023
ef8d221
Bugfixes for TTS (#390)
mickael-menu Sep 15, 2023
f85d408
Add a download manager (#381)
qnga Sep 15, 2023
4f4fec1
Bugfixes for TTS (#391)
mickael-menu Sep 15, 2023
e09da40
Write licenses into ZIP through shared storage (#389)
qnga Sep 18, 2023
01c4f58
Merge branch 'v3' into refactor-href-url
mickael-menu Sep 18, 2023
dbf09e2
Rename test app
mickael-menu Sep 18, 2023
65c7c80
Address review comments
mickael-menu Sep 18, 2023
708b18c
Refactor Link href resolution
mickael-menu Sep 18, 2023
d505939
Nullable `Url` path
mickael-menu Sep 19, 2023
e840fc0
Improve error reporting
mickael-menu Sep 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ 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
import org.readium.r2.shared.util.toFile

internal class LcpContentProtection(
private val lcpService: LcpService,
Expand Down Expand Up @@ -107,7 +107,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.href() to enc }
}
.toMap()

servicesBuilder.contentProtectionServiceFactory = serviceFactory
Expand Down Expand Up @@ -145,7 +147,7 @@ internal class LcpContentProtection(
}

val link = checkNotNull(licenseDoc.link(LicenseDocument.Rel.Publication))
val url = Url(link.url.toString())
val url = (link.href() as? AbsoluteUrl)
?: return Try.failure(
Publication.OpeningException.ParsingFailed(
ThrowableError(
Expand All @@ -154,13 +156,22 @@ internal class LcpContentProtection(
)
)

return assetRetriever.retrieve(
url,
mediaType = link.mediaType,
assetType = AssetType.Archive
)
.mapFailure { Publication.OpeningException.ParsingFailed(it) }
.flatMap { createResultAsset(it as Asset.Container, license) }
val asset =
if (link.mediaType != null) {
assetRetriever.retrieve(
url,
mediaType = link.mediaType,
assetType = AssetType.Archive
)
.map { it as Asset.Container }
.mapFailure { Publication.OpeningException.ParsingFailed(it) }
} else {
(assetRetriever.retrieve(url) as? Asset.Container)
?.let { Try.success(it) }
?: Try.failure(Publication.OpeningException.ParsingFailed())
}

return asset.flatMap { createResultAsset(it, license) }
}

private fun ResourceFactory.Error.wrap(): Publication.OpeningException =
Expand Down
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 @@ -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.href() 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.href() 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
10 changes: 4 additions & 6 deletions readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ package org.readium.r2.lcp.license

import java.net.HttpURLConnection
import java.util.*
import kotlin.time.ExperimentalTime
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
Expand All @@ -33,7 +32,6 @@ import org.readium.r2.shared.util.getOrThrow
import org.readium.r2.shared.util.mediatype.MediaType
import timber.log.Timber

@OptIn(ExperimentalTime::class)
internal class License(
private var documents: ValidatedDocuments,
private val validation: LicenseValidation,
Expand Down Expand Up @@ -163,7 +161,7 @@ internal class License(
// Programmatically renew the loan with a PUT request.
suspend fun renewProgrammatically(link: Link): ByteArray {
val endDate =
if (link.templateParameters.contains("end")) {
if (link.href.parameters?.contains("end") == true) {
listener.preferredEndDate(maxRenewDate)
} else {
null
Expand All @@ -174,7 +172,7 @@ internal class License(
parameters["end"] = endDate.toIso8601String()
}

val url = link.url(parameters)
val url = link.href(parameters = parameters)

return network.fetch(url.toString(), NetworkService.Method.PUT)
.getOrElse { error ->
Expand All @@ -191,7 +189,7 @@ internal class License(
// Renew the loan by presenting a web page to the user.
suspend fun renewWithWebPage(link: Link): ByteArray {
// The reading app will open the URL in a web view and return when it is dismissed.
listener.openWebPage(link.url)
listener.openWebPage(link.href())

val statusURL = tryOrNull {
license.url(
Expand All @@ -211,7 +209,7 @@ internal class License(
?: throw LcpException.LicenseInteractionNotAvailable

val data =
if (link.mediaType.isHtml) {
if (link.mediaType?.isHtml == true) {
renewWithWebPage(link)
} else {
renewProgrammatically(link)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,35 @@ import org.readium.r2.lcp.LcpException
import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.shared.resource.Container
import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.getOrThrow

/**
* Access to a License Document stored in a read-only container.
*/
internal class ContainerLicenseContainer(
private val container: Container,
private val entryPath: String
private val entryUrl: Url
) : LicenseContainer {

override fun read(): ByteArray {
return runBlocking {
container
.get(entryPath)
.get(entryUrl)
.read()
.mapFailure {
when (it) {
is Resource.Exception.NotFound -> LcpException.Container.FileNotFound(
entryPath
)
else -> LcpException.Container.ReadFailed(entryPath)
is Resource.Exception.NotFound ->
LcpException.Container.FileNotFound(entryUrl)
else ->
LcpException.Container.ReadFailed(entryUrl)
}
}
.getOrThrow()
}
}

override fun write(license: LicenseDocument) {
throw LcpException.Container.WriteFailed(entryPath)
throw LcpException.Container.WriteFailed(entryUrl)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,25 @@ package org.readium.r2.lcp.license.container
import java.io.File
import org.readium.r2.lcp.LcpException
import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.shared.util.toUrl

/**
* Access a License Document stored in an LCP License Document file (LCPL).
*/
internal class LCPLLicenseContainer(private val lcpl: String) : LicenseContainer {
internal class LCPLLicenseContainer(private val lcpl: File) : LicenseContainer {

override fun read(): ByteArray =
try {
File(lcpl).readBytes()
lcpl.readBytes()
} catch (e: Exception) {
throw LcpException.Container.OpenFailed
}

override fun write(license: LicenseDocument) {
try {
File(lcpl).writeBytes(license.data)
lcpl.writeBytes(license.data)
} catch (e: Exception) {
throw LcpException.Container.WriteFailed(lcpl)
throw LcpException.Container.WriteFailed(lcpl.toUrl())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ internal class LcplResourceLicenseContainer(private val resource: Resource) : Li
}

override fun write(license: LicenseDocument) {
throw LcpException.Container.WriteFailed("")
throw LcpException.Container.WriteFailed(null)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.shared.asset.Asset
import org.readium.r2.shared.resource.Container
import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.mediatype.MediaType

private const val LICENSE_IN_EPUB = "META-INF/license.lcpl"

private const val LICENSE_IN_RPF = "license.lcpl"
private val LICENSE_IN_EPUB = Url("META-INF/license.lcpl")!!
private val LICENSE_IN_RPF = Url("license.lcpl")!!

/**
* Encapsulates the read/write access to the packaged License Document (eg. in an EPUB container,
Expand All @@ -35,10 +35,10 @@ internal fun createLicenseContainer(
mediaType: MediaType
): LicenseContainer =
when (mediaType) {
MediaType.EPUB -> ZIPLicenseContainer(file.path, LICENSE_IN_EPUB)
MediaType.LCP_LICENSE_DOCUMENT -> LCPLLicenseContainer(file.path)
MediaType.EPUB -> ZIPLicenseContainer(file, LICENSE_IN_EPUB)
MediaType.LCP_LICENSE_DOCUMENT -> LCPLLicenseContainer(file)
// Assuming it's a Readium WebPub package (e.g. audiobook, LCPDF, etc.) as a fallback
else -> ZIPLicenseContainer(file.path, LICENSE_IN_RPF)
else -> ZIPLicenseContainer(file, LICENSE_IN_RPF)
}

internal fun createLicenseContainer(
Expand All @@ -63,10 +63,10 @@ internal fun createLicenseContainer(
container: Container,
mediaType: MediaType
): LicenseContainer {
val licensePath = when (mediaType) {
val licenseUrl = when (mediaType) {
MediaType.EPUB -> LICENSE_IN_EPUB
// Assuming it's a Readium WebPub package (e.g. audiobook, LCPDF, etc.) as a fallback
else -> LICENSE_IN_RPF
}
return ContainerLicenseContainer(container, licensePath)
return ContainerLicenseContainer(container, licenseUrl)
}
Loading