Skip to content
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

Refactor media type sniffing #377

Merged
merged 24 commits into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
476a3e6
Refactor media types
mickael-menu Aug 18, 2023
2cfc2e7
Implement the media type in resources thanks to the format registry
mickael-menu Aug 18, 2023
de0a9e1
Optimization
mickael-menu Aug 18, 2023
14d8e02
Remove legacy code
mickael-menu Aug 18, 2023
c27c939
Fix reading remote web publication
mickael-menu Aug 20, 2023
bf320f3
Fix database naming convention in the test app
mickael-menu Aug 20, 2023
a67f52d
Move multi rounds sniffing strategy to `OptimizedRoundsMediaTypeSniffer`
mickael-menu Aug 21, 2023
1fdf5d3
Remove Format from Asset
mickael-menu Aug 21, 2023
af68130
Restore default HTTP client in the OPDS parsers
mickael-menu Aug 21, 2023
3a54463
Simplify `Format`
mickael-menu Aug 21, 2023
d065398
Make `Resource.mediaType` non nullable
mickael-menu Aug 21, 2023
05a7017
`MediaTypeSnifferContext` is not closeable
mickael-menu Aug 21, 2023
595add0
Fix tests
mickael-menu Aug 21, 2023
05a09bd
Make system and content URI sniffing generic
mickael-menu Aug 21, 2023
26eec03
Fix lint
mickael-menu Aug 21, 2023
49f6cbc
Simplify `FormatRegistry`
mickael-menu Aug 21, 2023
25bc2d3
Make sure we always return canonical media types, and separate light …
mickael-menu Aug 21, 2023
1c8d360
Address review comments
mickael-menu Aug 21, 2023
866b93b
Fix `ParserAssetFactory`
mickael-menu Aug 21, 2023
b516c69
Reintroduce `MediaTypeRetriever`
mickael-menu Aug 22, 2023
9afa5ef
Remove `ZipContainer`
mickael-menu Aug 22, 2023
0c2fce1
Fix tests
mickael-menu Aug 22, 2023
be26f9f
Fix `AssetRetriever`
mickael-menu Aug 22, 2023
801266d
Add documentation
mickael-menu Aug 22, 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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,5 @@ lint/reports/
docs/readium
docs/index.md
docs/package-list
site/
site/
androidTestResultsUserPreferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ package org.readium.r2.lcp
import org.readium.r2.lcp.auth.LcpPassphraseAuthentication
import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.shared.asset.Asset
import org.readium.r2.shared.asset.AssetRetriever
import org.readium.r2.shared.asset.AssetType
import org.readium.r2.shared.error.ThrowableError
import org.readium.r2.shared.error.Try
import org.readium.r2.shared.error.flatMap
import org.readium.r2.shared.error.getOrElse
import org.readium.r2.shared.publication.Publication
import org.readium.r2.shared.publication.encryption.encryption
Expand All @@ -27,8 +30,7 @@ import org.readium.r2.shared.util.toFile
internal class LcpContentProtection(
private val lcpService: LcpService,
private val authentication: LcpAuthenticating,
private val resourceFactory: ResourceFactory,
private val archiveFactory: ArchiveFactory
private val assetRetriever: AssetRetriever
) : ContentProtection {

override val scheme: ContentProtection.Scheme =
Expand Down Expand Up @@ -152,15 +154,13 @@ internal class LcpContentProtection(
)
)

val resource = resourceFactory.create(url)
.getOrElse { return Try.failure(it.wrap()) }

val container = archiveFactory.create(resource, password = null)
.getOrElse { return Try.failure(it.wrap()) }

val publicationAsset = Asset.Container(link.mediaType, exploded = false, container)

return createResultAsset(publicationAsset, license)
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 =
Expand Down
24 changes: 10 additions & 14 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,9 @@ import org.readium.r2.lcp.service.NetworkService
import org.readium.r2.lcp.service.PassphrasesRepository
import org.readium.r2.lcp.service.PassphrasesService
import org.readium.r2.shared.asset.Asset
import org.readium.r2.shared.asset.AssetRetriever
import org.readium.r2.shared.error.Try
import org.readium.r2.shared.publication.protection.ContentProtection
import org.readium.r2.shared.resource.ArchiveFactory
import org.readium.r2.shared.resource.DefaultArchiveFactory
import org.readium.r2.shared.resource.FileResourceFactory
import org.readium.r2.shared.resource.ResourceFactory
import org.readium.r2.shared.util.mediatype.MediaType
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever

Expand Down Expand Up @@ -159,9 +156,8 @@ public interface LcpService {
*/
public operator fun invoke(
context: Context,
mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(),
resourceFactory: ResourceFactory = FileResourceFactory(),
archiveFactory: ArchiveFactory = DefaultArchiveFactory()
assetRetriever: AssetRetriever,
mediaTypeRetriever: MediaTypeRetriever
): LcpService? {
if (!LcpClient.isAvailable()) {
return null
Expand All @@ -171,7 +167,7 @@ public interface LcpService {
val deviceRepository = DeviceRepository(db)
val passphraseRepository = PassphrasesRepository(db)
val licenseRepository = LicensesRepository(db)
val network = NetworkService(mediaTypeRetriever = mediaTypeRetriever)
val network = NetworkService(mediaTypeRetriever)
val device = DeviceService(
repository = deviceRepository,
network = network,
Expand All @@ -186,18 +182,17 @@ public interface LcpService {
network = network,
passphrases = passphrases,
context = context,
mediaTypeRetriever = mediaTypeRetriever,
resourceFactory = resourceFactory,
archiveFactory = archiveFactory
assetRetriever = assetRetriever
)
}

@Suppress("UNUSED_PARAMETER")
@Deprecated(
"Use `LcpService()` instead",
ReplaceWith("LcpService(context)"),
ReplaceWith("LcpService(context, AssetRetriever(), MediaTypeRetriever())"),
level = DeprecationLevel.ERROR
)
public fun create(context: Context): LcpService? = invoke(context)
public fun create(context: Context): LcpService? = throw NotImplementedError()
}

@Deprecated(
Expand Down Expand Up @@ -235,13 +230,14 @@ public interface LcpService {
}
}

@Suppress("UNUSED_PARAMETER")
@Deprecated(
"Renamed to `LcpService()`",
replaceWith = ReplaceWith("LcpService(context)"),
level = DeprecationLevel.ERROR
)
public fun R2MakeLCPService(context: Context): LcpService =
LcpService(context) ?: throw Exception("liblcp is missing on the classpath")
throw NotImplementedError()

@Deprecated(
"Renamed to `LcpService.AcquiredPublication`",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ internal class LicenseValidation(
StatusDocument.Status.Returned -> LcpException.LicenseStatus.Returned(date)
StatusDocument.Status.Revoked -> {
val devicesCount = status.events(
org.readium.r2.lcp.license.model.components.lsd.Event.EventType.register
org.readium.r2.lcp.license.model.components.lsd.Event.EventType.Register
).size
LcpException.LicenseStatus.Revoked(date, devicesCount = devicesCount)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public data class Link(val json: JSONObject) {
get() = url(parameters = emptyMap())

val mediaType: MediaType
get() = type?.let { MediaType.parse(it) } ?: MediaType.BINARY
get() = type?.let { MediaType(it) } ?: MediaType.BINARY

/**
* List of URI template parameter keys, if the [Link] is templated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ public data class Event(val json: JSONObject) {
val date: Date? = json.optNullableString("timestamp")?.iso8601ToDate()

public enum class EventType(public val value: String) {
register("register"),
renew("renew"),
`return`("return"),
revoke("revoke"),
cancel("cancel");
Register("register"),
Renew("renew"),
Return("return"),
Revoke("revoke"),
Cancel("cancel");

@Deprecated("Use [value] instead", ReplaceWith("value"), level = DeprecationLevel.ERROR)
public val rawValue: String get() = value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@ public typealias LCPError = LcpException
ReplaceWith("LcpService()"),
level = DeprecationLevel.ERROR
)
@Suppress("UNUSED_PARAMETER")
public fun R2MakeLCPService(context: Context): LcpService? =
LcpService(context)
throw NotImplementedError()
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,11 @@ import org.readium.r2.lcp.license.container.LicenseContainer
import org.readium.r2.lcp.license.container.createLicenseContainer
import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.shared.asset.Asset
import org.readium.r2.shared.asset.AssetRetriever
import org.readium.r2.shared.error.Try
import org.readium.r2.shared.extensions.tryOr
import org.readium.r2.shared.publication.protection.ContentProtection
import org.readium.r2.shared.resource.ArchiveFactory
import org.readium.r2.shared.resource.ResourceFactory
import org.readium.r2.shared.util.mediatype.MediaType
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever
import timber.log.Timber

internal class LicensesService(
Expand All @@ -46,17 +44,13 @@ internal class LicensesService(
private val network: NetworkService,
private val passphrases: PassphrasesService,
private val context: Context,
private val mediaTypeRetriever: MediaTypeRetriever,
private val resourceFactory: ResourceFactory,
private val archiveFactory: ArchiveFactory
private val assetRetriever: AssetRetriever
) : LcpService, CoroutineScope by MainScope() {

override suspend fun isLcpProtected(file: File): Boolean =
tryOr(false) {
val mediaType = mediaTypeRetriever.retrieve(file) ?: return false
createLicenseContainer(file, mediaType).read()
true
}
override suspend fun isLcpProtected(file: File): Boolean {
val asset = assetRetriever.retrieve(file) ?: return false
return isLcpProtected(asset)
}

override suspend fun isLcpProtected(asset: Asset): Boolean =
tryOr(false) {
Expand All @@ -73,7 +67,7 @@ internal class LicensesService(
override fun contentProtection(
authentication: LcpAuthenticating
): ContentProtection =
LcpContentProtection(this, authentication, resourceFactory, archiveFactory)
LcpContentProtection(this, authentication, assetRetriever)

override suspend fun acquirePublication(lcpl: ByteArray, onProgress: (Double) -> Unit): Try<LcpService.AcquiredPublication, LcpException> =
try {
Expand Down Expand Up @@ -251,9 +245,7 @@ internal class LicensesService(
destination,
mediaType = link.type,
onProgress = onProgress
)
?: mediaTypeRetriever.retrieve(mediaType = link.type)
?: MediaType.EPUB
) ?: link.mediaType

// Saves the License Document into the downloaded publication
val container = createLicenseContainer(destination, mediaType)
Expand All @@ -266,4 +258,14 @@ internal class LicensesService(
licenseDocument = license
)
}

private val MediaType.fileExtension: String get() =
when {
matches(MediaType.DIVINA) -> "divina"
matches(MediaType.EPUB) -> "epub"
matches(MediaType.LCP_PROTECTED_PDF) -> "pdf"
matches(MediaType.READIUM_AUDIOBOOK) -> "audiobook"
matches(MediaType.READIUM_WEBPUB) -> "webpub"
else -> "epub"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.readium.r2.lcp.LcpException
import org.readium.r2.shared.error.Try
import org.readium.r2.shared.util.http.retrieve
import org.readium.r2.shared.util.http.invoke
import org.readium.r2.shared.util.mediatype.MediaType
import org.readium.r2.shared.util.mediatype.MediaTypeHints
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever
import timber.log.Timber

Expand Down Expand Up @@ -138,10 +139,7 @@ internal class NetworkService(
}
}

mediaTypeRetriever.retrieve(
connection = connection,
mediaType = mediaType
)
mediaTypeRetriever.retrieve(MediaTypeHints(connection, mediaType = mediaType))
} catch (e: Exception) {
Timber.e(e)
throw LcpException.Network(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,3 @@ package org.readium.r2.navigator.divina
level = DeprecationLevel.ERROR
)
public open class R2DiViNaActivity

// This is for lint to pass.
private val fake = null
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,7 @@ public class EpubNavigatorFragment internal constructor(
return currentReflowablePageFragment?.webView?.findFirstVisibleLocator()
?.copy(
href = resource.href,
type = resource.type ?: MediaType.XHTML.toString()
type = (resource.mediaType ?: MediaType.XHTML).toString()
)
}

Expand Down Expand Up @@ -1026,7 +1026,7 @@ public class EpubNavigatorFragment internal constructor(

val currentLocator = Locator(
href = link.href,
type = link.type ?: MediaType.XHTML.toString(),
type = (link.mediaType ?: MediaType.XHTML).toString(),
title = tableOfContentsTitleByHref[link.href] ?: positionLocator?.title ?: link.title,
locations = (positionLocator?.locations ?: Locator.Locations()).copy(
progression = progression
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ internal fun Resource.injectHtml(
.getOrElse {
return@TransformingResource ResourceTry.failure(it)
}
?.takeIf { it.isHtml }
.takeIf { it.isHtml }
?: return@TransformingResource ResourceTry.success(bytes)

var content = bytes.toString(mediaType.charset ?: Charsets.UTF_8).trim()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ internal class WebViewServer(

var resource = publication.get(linkWithoutAnchor)
.fallback { errorResource(link, error = it) }
if (link.mediaType.isHtml) {
if (link.mediaType?.isHtml == true) {
resource = resource.injectHtml(
publication,
css,
Expand All @@ -105,7 +105,7 @@ internal class WebViewServer(

if (range == null) {
return WebResourceResponse(
link.type,
link.mediaType?.toString(),
null,
200,
"OK",
Expand All @@ -119,7 +119,14 @@ internal class WebViewServer(
headers["Content-Range"] = "bytes ${longRange.first}-${longRange.last}/$length"
// Content-Length will automatically be filled by the WebView using the Content-Range header.
// headers["Content-Length"] = (longRange.last - longRange.first + 1).toString()
return WebResourceResponse(link.type, null, 206, "Partial Content", headers, stream)
return WebResourceResponse(
link.mediaType?.toString(),
null,
206,
"Partial Content",
headers,
stream
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public class PdfNavigatorFragment<S : Configurable.Settings, P : Configurable.Pr

require(
publication.readingOrder.count() == 1 &&
publication.readingOrder.first().mediaType.matches(MediaType.PDF)
publication.readingOrder.first().mediaType?.matches(MediaType.PDF) == true
) { "[PdfNavigatorFragment] currently supports only publications with a single PDF for reading order" }
}

Expand Down
Loading