From 10fe08eac56a90945e6171b94eb394dedd464e54 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Tue, 24 Oct 2023 11:57:42 +0200 Subject: [PATCH 01/86] Revisit factories --- .../org/readium/r2/shared/util/asset/Asset.kt | 16 +-- .../r2/shared/util/asset/AssetRetriever.kt | 59 +++------ .../shared/util/http/HttpResourceFactory.kt | 15 ++- .../r2/shared/util/resource/Container.kt | 2 +- .../shared/util/resource/ContentResource.kt | 27 +++- .../util/resource/DirectoryContainer.kt | 55 ++++---- .../r2/shared/util/resource/Factories.kt | 117 ++++++++---------- .../r2/shared/util/resource/FileResource.kt | 20 ++- .../util/resource/FileZipArchiveFactory.kt | 21 +++- .../util/zip/StreamingZipArchiveFactory.kt | 19 +++ 10 files changed, 206 insertions(+), 145 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt index af5e850e5d..2a54d423be 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt @@ -18,7 +18,7 @@ public sealed class Asset { /** * Type of the asset source. */ - public abstract val assetType: org.readium.r2.shared.util.asset.AssetType + public abstract val assetType: AssetType /** * Media type of the asset. @@ -39,10 +39,10 @@ public sealed class Asset { public class Resource( override val mediaType: MediaType, public val resource: SharedResource - ) : org.readium.r2.shared.util.asset.Asset() { + ) : Asset() { - override val assetType: org.readium.r2.shared.util.asset.AssetType = - org.readium.r2.shared.util.asset.AssetType.Resource + override val assetType: AssetType = + AssetType.Resource override suspend fun close() { resource.close() @@ -60,13 +60,13 @@ public sealed class Asset { override val mediaType: MediaType, exploded: Boolean, public val container: SharedContainer - ) : org.readium.r2.shared.util.asset.Asset() { + ) : Asset() { - override val assetType: org.readium.r2.shared.util.asset.AssetType = + override val assetType: AssetType = if (exploded) { - org.readium.r2.shared.util.asset.AssetType.Directory + AssetType.Directory } else { - org.readium.r2.shared.util.asset.AssetType.Archive + AssetType.Archive } override suspend fun close() { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 10f0e2c915..48dc41786f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -11,6 +11,12 @@ import android.content.Context import android.net.Uri import android.provider.MediaStore import java.io.File +import kotlin.Boolean +import kotlin.Exception +import kotlin.String +import kotlin.let +import kotlin.run +import kotlin.takeUnless import org.readium.r2.shared.extensions.queryProjection import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Either @@ -19,7 +25,6 @@ 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.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeRetriever @@ -147,9 +152,9 @@ public class AssetRetriever( url: AbsoluteUrl, mediaType: MediaType ): Try { - return retrieveResource(url) + return retrieveResource(url, mediaType) .flatMap { resource: Resource -> - archiveFactory.create(resource, password = null) + archiveFactory.create(resource, password = null, mediaType) .mapFailure { error -> when (error) { is ArchiveFactory.Error.FormatNotSupported -> @@ -178,7 +183,7 @@ public class AssetRetriever( url: AbsoluteUrl, mediaType: MediaType ): Try { - return containerFactory.create(url) + return containerFactory.create(url, mediaType) .map { container -> Asset.Container( mediaType, @@ -188,16 +193,6 @@ public class AssetRetriever( } .mapFailure { error -> when (error) { - is ContainerFactory.Error.NotAContainer -> - Error.NotFound( - url, - error - ) - is ContainerFactory.Error.Forbidden -> - Error.Forbidden( - url, - error - ) is ContainerFactory.Error.SchemeNotSupported -> Error.SchemeNotSupported( error.scheme, @@ -211,7 +206,7 @@ public class AssetRetriever( url: AbsoluteUrl, mediaType: MediaType ): Try { - return retrieveResource(url) + return retrieveResource(url, mediaType) .map { resource -> Asset.Resource( mediaType, @@ -221,21 +216,12 @@ public class AssetRetriever( } private suspend fun retrieveResource( - url: AbsoluteUrl + url: AbsoluteUrl, + mediaType: MediaType ): Try { - return resourceFactory.create(url) + return resourceFactory.create(url, mediaType) .mapFailure { error -> when (error) { - is ResourceFactory.Error.NotAResource -> - Error.NotFound( - url, - error - ) - is ResourceFactory.Error.Forbidden -> - Error.Forbidden( - url, - error - ) is ResourceFactory.Error.SchemeNotSupported -> Error.SchemeNotSupported( error.scheme, @@ -297,22 +283,15 @@ public class AssetRetriever( public suspend fun retrieve(url: Url): Asset? { if (url !is AbsoluteUrl) return null - val resource = resourceFactory - .create(url) - .getOrElse { error -> - when (error) { - is ResourceFactory.Error.NotAResource -> - return containerFactory.create(url).getOrNull() - ?.let { retrieve(url, it, exploded = true) } - else -> return null - } + val resource = resourceFactory.create(url) + ?: run { + return containerFactory.create(url) + ?.let { retrieve(url, it, exploded = true) } } return archiveFactory.create(resource, password = null) - .fold( - { retrieve(url, container = it, exploded = false) }, - { retrieve(url, resource) } - ) + ?.let { retrieve(url, container = it, exploded = false) } + ?: retrieve(url, resource) } private suspend fun retrieve( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt index 0552d4e78f..6abab21b05 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt @@ -8,6 +8,7 @@ package org.readium.r2.shared.util.http import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceFactory @@ -15,7 +16,19 @@ public class HttpResourceFactory( private val httpClient: HttpClient ) : ResourceFactory { - override suspend fun create(url: AbsoluteUrl): Try { + override suspend fun create(url: AbsoluteUrl): Resource? { + if (!url.isHttp) { + return null + } + + // FIXME: should make a head request to check that url points to a resource + return HttpResource(httpClient, url) + } + + override suspend fun create( + url: AbsoluteUrl, + mediaType: MediaType + ): Try { if (!url.isHttp) { return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt index db476f78ef..5cc9a2d487 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt @@ -76,7 +76,7 @@ public class ResourceContainer(url: Url, resource: Resource) : Container { } } -/** Convenience helper to wrap a [Resource] and a [path] into a [Container.Entry]. */ +/** Convenience helper to wrap a [Resource] and a [url] into a [Container.Entry]. */ internal fun Resource.toEntry(url: Url): Container.Entry = object : Container.Entry, Resource by this { override val url: Url = url diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt index bc40f9dc81..5e1b9d0434 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt @@ -28,12 +28,24 @@ public class ContentResourceFactory( private val contentResolver: ContentResolver ) : ResourceFactory { - override suspend fun create(url: AbsoluteUrl): Try { + override suspend fun create(url: AbsoluteUrl): Resource? { + if (!url.isContent) { + return null + } + + // FIXME: should check if uri points t o a file + return ContentResource(url.toUri(), contentResolver) + } + + override suspend fun create( + url: AbsoluteUrl, + mediaType: MediaType + ): Try { if (!url.isContent) { return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) } - val resource = ContentResource(url.toUri(), contentResolver) + val resource = ContentResource(url.toUri(), contentResolver, mediaType) return Try.success(resource) } @@ -42,9 +54,10 @@ public class ContentResourceFactory( /** * A [Resource] to access content [uri] thanks to a [ContentResolver]. */ -public class ContentResource( +public class ContentResource internal constructor( private val uri: Uri, - private val contentResolver: ContentResolver + private val contentResolver: ContentResolver, + private val mediaType: MediaType? = null ) : Resource { private lateinit var _length: ResourceTry @@ -55,7 +68,11 @@ public class ContentResource( ResourceTry.success(Resource.Properties()) override suspend fun mediaType(): ResourceTry = - Try.success(contentResolver.getType(uri)?.let { MediaType(it) } ?: MediaType.BINARY) + Try.success( + mediaType + ?: contentResolver.getType(uri)?.let { MediaType(it) } + ?: MediaType.BINARY + ) override suspend fun close() { } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index 11212b834e..f14550df50 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -11,10 +11,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.isParentOf 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.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever /** @@ -22,7 +24,6 @@ import org.readium.r2.shared.util.mediatype.MediaTypeRetriever */ internal class DirectoryContainer( private val root: File, - private val entries: List, private val mediaTypeRetriever: MediaTypeRetriever ) : Container { @@ -32,11 +33,24 @@ internal class DirectoryContainer( override suspend fun close() {} } - override suspend fun entries(): Set = - entries.mapNotNull { file -> - Url.fromDecodedPath(file.relativeTo(root).path) - ?.let { url -> FileEntry(url, file) } - }.toSet() + private val _entries: Set? by lazy { + tryOrNull { + root.walk() + .filter { it.isFile } + .mapNotNull { it.toEntry() } + .toSet() + } + } + + private fun File.toEntry(): Container.Entry? = + Url.fromDecodedPath(this.relativeTo(root).path) + ?.let { url -> FileEntry(url, this) } + + override suspend fun entries(): Set? { + return withContext(Dispatchers.IO) { + _entries + } + } override fun get(url: Url): Container.Entry { val file = (url as? RelativeUrl)?.path @@ -56,31 +70,30 @@ public class DirectoryContainerFactory( private val mediaTypeRetriever: MediaTypeRetriever ) : ContainerFactory { - override suspend fun create(url: AbsoluteUrl): Try { + override suspend fun create(url: AbsoluteUrl): Container? { val file = url.toFile() - ?: return Try.failure(ContainerFactory.Error.SchemeNotSupported(url.scheme)) + ?: return null if (!tryOr(false) { file.isDirectory }) { - return Try.failure(ContainerFactory.Error.NotAContainer(url)) + return null } + return create(file).getOrNull() + } + + override suspend fun create( + url: AbsoluteUrl, + mediaType: MediaType + ): Try { + val file = url.toFile() + ?: return Try.failure(ContainerFactory.Error.SchemeNotSupported(url.scheme)) + return create(file) } // Internal for testing purpose internal suspend fun create(file: File): Try { - val entries = - try { - withContext(Dispatchers.IO) { - file.walk() - .filter { it.isFile } - .toList() - } - } catch (e: Exception) { - return Try.failure(ContainerFactory.Error.Forbidden(e)) - } - - val container = DirectoryContainer(file, entries, mediaTypeRetriever) + val container = DirectoryContainer(file, mediaTypeRetriever) return Try.success(container) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt index 937569fb67..6f820c8ad1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt @@ -11,6 +11,7 @@ import org.readium.r2.shared.util.Error as SharedError 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.mediatype.MediaType import org.readium.r2.shared.util.tryRecover /** @@ -19,7 +20,7 @@ import org.readium.r2.shared.util.tryRecover * An exception must be returned if the url scheme is not supported or * the resource cannot be found. */ -public fun interface ResourceFactory { +public interface ResourceFactory { public sealed class Error : SharedError { @@ -36,33 +37,11 @@ public fun interface ResourceFactory { override val message: String = "Url scheme $scheme is not supported." } - - public class NotAResource( - public val url: AbsoluteUrl, - override val cause: SharedError? = null - ) : Error() { - - public constructor(url: AbsoluteUrl, exception: Exception) : this( - url, - ThrowableError(exception) - ) - - override val message: String = - "No resource found at url $url." - } - - public class Forbidden( - override val cause: SharedError - ) : Error() { - - public constructor(exception: Exception) : this(ThrowableError(exception)) - - override val message: String = - "Access to the container is forbidden." - } } - public suspend fun create(url: AbsoluteUrl): Try + public suspend fun create(url: AbsoluteUrl): Resource? + + public suspend fun create(url: AbsoluteUrl, mediaType: MediaType): Try } /** @@ -71,7 +50,7 @@ public fun interface ResourceFactory { * An exception must be returned if the url scheme is not supported or * the url doesn't seem to point to a container. */ -public fun interface ContainerFactory { +public interface ContainerFactory { public sealed class Error : SharedError { @@ -88,41 +67,28 @@ public fun interface ContainerFactory { override val message: String = "Url scheme $scheme is not supported." } - - public class NotAContainer( - public val url: Url, - override val cause: SharedError? = null - ) : Error() { - - public constructor(url: Url, exception: Exception) : this( - url, - ThrowableError(exception) - ) - - override val message: String = - "No container found at url $url." - } - - public class Forbidden( - override val cause: SharedError - ) : Error() { - - public constructor(exception: Exception) : this(ThrowableError(exception)) - - override val message: String = - "Access to the container is forbidden." - } } - public suspend fun create(url: AbsoluteUrl): Try + /** + * Returns a [Container] to access the content if this factory claims that [url] points to + * a resource it can provide access to and null otherwise. + */ + public suspend fun create(url: AbsoluteUrl): Container? + + /** + * Tries to create a [Container] giving access to a [Url] known to point to a directory + * with the given [mediaType]. + * + * An error must be returned if the url scheme or media type is not supported. + */ + public suspend fun create(url: AbsoluteUrl, mediaType: MediaType): Try } /** * A factory to create [Container]s from archive [Resource]s. * - * An exception must be returned if the resource type, password or media type is not supported. */ -public fun interface ArchiveFactory { +public interface ArchiveFactory { public sealed class Error( override val message: String, @@ -155,7 +121,19 @@ public fun interface ArchiveFactory { } } - public suspend fun create(resource: Resource, password: String?): Try + /** + * Returns a [Container] to access the archive content if this factory claims that [resource] is + * an archive that it supports and null otherwise. + */ + public suspend fun create(resource: Resource, password: String?): Container? + + /** + * Tries to create a [Container] from a [Resource] known to be an archive + * with the given [mediaType]. + * + * An error must be returned if the resource type, password or media type is not supported. + */ + public suspend fun create(resource: Resource, password: String?, mediaType: MediaType): Try } /** @@ -167,11 +145,16 @@ public class CompositeArchiveFactory( private val fallbackFactory: ArchiveFactory ) : ArchiveFactory { - override suspend fun create(resource: Resource, password: String?): Try { + override suspend fun create(resource: Resource, password: String?): Container? { return primaryFactory.create(resource, password) + ?: fallbackFactory.create(resource, password) + } + + override suspend fun create(resource: Resource, password: String?, mediaType: MediaType): Try { + return primaryFactory.create(resource, password, mediaType) .tryRecover { error -> if (error is ArchiveFactory.Error.FormatNotSupported) { - fallbackFactory.create(resource, password) + fallbackFactory.create(resource, password, mediaType) } else { Try.failure(error) } @@ -188,11 +171,15 @@ public class CompositeResourceFactory( private val fallbackFactory: ResourceFactory ) : ResourceFactory { - override suspend fun create(url: AbsoluteUrl): Try { - return primaryFactory.create(url) + override suspend fun create(url: AbsoluteUrl): Resource? { + return primaryFactory.create(url) ?: fallbackFactory.create(url) + } + + override suspend fun create(url: AbsoluteUrl, mediaType: MediaType): Try { + return primaryFactory.create(url, mediaType) .tryRecover { error -> if (error is ResourceFactory.Error.SchemeNotSupported) { - fallbackFactory.create(url) + fallbackFactory.create(url, mediaType) } else { Try.failure(error) } @@ -209,11 +196,15 @@ public class CompositeContainerFactory( private val fallbackFactory: ContainerFactory ) : ContainerFactory { - override suspend fun create(url: AbsoluteUrl): Try { - return primaryFactory.create(url) + override suspend fun create(url: AbsoluteUrl): Container? { + return primaryFactory.create(url) ?: fallbackFactory.create(url) + } + + override suspend fun create(url: AbsoluteUrl, mediaType: MediaType): Try { + return primaryFactory.create(url, mediaType) .tryRecover { error -> if (error is ContainerFactory.Error.SchemeNotSupported) { - fallbackFactory.create(url) + fallbackFactory.create(url, mediaType) } else { Try.failure(error) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt index d190ee4762..a0f7d4db6a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt @@ -136,18 +136,28 @@ public class FileResourceFactory( private val mediaTypeRetriever: MediaTypeRetriever ) : ResourceFactory { - override suspend fun create(url: AbsoluteUrl): Try { + override suspend fun create(url: AbsoluteUrl): Resource? { val file = url.toFile() - ?: return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) + ?: return null try { if (!file.isFile) { - return Try.failure(ResourceFactory.Error.NotAResource(url)) + return null } } catch (e: Exception) { - return Try.failure(ResourceFactory.Error.Forbidden(e)) + return null } - return Try.success(FileResource(file, mediaTypeRetriever)) + return FileResource(file, mediaTypeRetriever) + } + + override suspend fun create( + url: AbsoluteUrl, + mediaType: MediaType + ): Try { + val file = url.toFile() + ?: return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) + + return Try.success(FileResource(file, mediaType)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt index 3c100f57fa..c564294374 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever /** @@ -22,11 +23,29 @@ public class FileZipArchiveFactory( private val mediaTypeRetriever: MediaTypeRetriever ) : ArchiveFactory { - override suspend fun create(resource: Resource, password: String?): Try { + override suspend fun create(resource: Resource, password: String?): Container? { + if (password != null) { + return null + } + + return resource.source?.toFile() + ?.let { open(it) } + ?.getOrNull() + } + + override suspend fun create( + resource: Resource, + password: String?, + mediaType: MediaType + ): Try { if (password != null) { return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) } + if (!mediaType.isZip) { + return Try.failure(ArchiveFactory.Error.FormatNotSupported()) + } + return resource.source?.toFile() ?.let { open(it) } ?: Try.Failure( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt index 9e75c84309..c39cfea1e8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt @@ -7,7 +7,9 @@ package org.readium.r2.shared.util.zip import java.io.File +import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.ArchiveFactory import org.readium.r2.shared.util.resource.Container @@ -27,6 +29,23 @@ public class StreamingZipArchiveFactory( override suspend fun create( resource: Resource, password: String? + ): Container? { + if (password != null) { + return null + } + + return tryOrNull { + val resourceChannel = ResourceChannel(resource) + val channel = wrapBaseChannel(resourceChannel) + val zipFile = ZipFile(channel, true) + ChannelZipContainer(zipFile, resource.source, mediaTypeRetriever) + } + } + + override suspend fun create( + resource: Resource, + password: String?, + mediaType: MediaType ): Try { if (password != null) { return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) From 8fa923c2cdc8ffb30f6f53610cfd4f145c8605ef Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Tue, 24 Oct 2023 15:28:57 +0200 Subject: [PATCH 02/86] WIP --- .../adapter/pdfium/document/PdfiumDocument.kt | 10 +- .../navigator/PdfiumDocumentFragment.kt | 2 +- .../pdfium/navigator/PdfiumEngineProvider.kt | 2 +- .../pspdfkit/document/PsPdfKitDocument.kt | 9 +- .../pspdfkit/document/ResourceDataProvider.kt | 4 +- .../navigator/PsPdfKitDocumentFragment.kt | 2 +- .../navigator/PsPdfKitEngineProvider.kt | 2 +- .../readium/r2/lcp/LcpContentProtection.kt | 58 ++--- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 3 +- .../org/readium/r2/lcp/license/License.kt | 1 + .../container/ContainerLicenseContainer.kt | 2 +- .../readium/r2/lcp/service/LicensesService.kt | 3 +- .../org/readium/r2/navigator/Navigator.kt | 2 +- .../r2/navigator/epub/WebViewServer.kt | 2 +- .../r2/navigator/media/ExoMediaPlayer.kt | 10 +- .../readium/r2/navigator/media/MediaPlayer.kt | 2 +- .../r2/navigator/media/MediaService.kt | 4 +- .../media/tts/session/TtsSessionAdapter.kt | 15 +- .../r2/shared/publication/Publication.kt | 85 +------ .../AdeptFallbackContentProtection.kt | 16 +- .../protection/ContentProtection.kt | 3 +- .../LcpFallbackContentProtection.kt | 16 +- .../services/ContentProtectionService.kt | 15 +- .../publication/services/CoverService.kt | 9 +- .../iterators/HtmlResourceContentIterator.kt | 1 + .../services/search/SearchService.kt | 61 ++--- .../services/search/StringSearchService.kt | 11 +- .../java/org/readium/r2/shared/util/Error.kt | 24 +- .../r2/shared/util/asset/AssetError.kt | 78 ++++++ .../r2/shared/util/asset/AssetRetriever.kt | 21 +- .../shared/util/downloads/DownloadManager.kt | 27 +-- .../android/AndroidDownloadManager.kt | 33 +-- .../foreground/ForegroundDownloadManager.kt | 50 +--- .../r2/shared/util/http/DefaultHttpClient.kt | 41 +++- .../readium/r2/shared/util/http/HttpClient.kt | 19 +- .../r2/shared/util/http/HttpContainer.kt | 4 +- .../http/{HttpException.kt => HttpError.kt} | 89 +++---- .../r2/shared/util/http/HttpResource.kt | 42 ++-- .../r2/shared/util/mediatype/Extensions.kt | 2 +- .../util/mediatype/MediaTypeSnifferContent.kt | 78 +++--- .../r2/shared/util/resource/BytesResource.kt | 7 +- .../r2/shared/util/resource/Container.kt | 4 +- .../shared/util/resource/ContentResource.kt | 21 +- .../util/resource/DirectoryContainer.kt | 2 +- .../r2/shared/util/resource/Factories.kt | 11 +- .../shared/util/resource/FallbackResource.kt | 4 +- .../util/resource/FileChannelResource.kt | 12 +- .../r2/shared/util/resource/FileResource.kt | 14 +- .../util/resource/FileZipArchiveFactory.kt | 4 +- .../r2/shared/util/resource/Resource.kt | 223 +++++++++++------- .../shared/util/resource/RoutingContainer.kt | 2 +- .../r2/shared/util/resource/ZipContainer.kt | 26 +- .../content/ResourceContentExtractor.kt | 26 +- .../r2/shared/util/zip/ChannelZipContainer.kt | 9 +- .../readium/r2/shared/util/zip/HttpChannel.kt | 8 +- .../r2/shared/util/zip/ResourceChannel.kt | 21 +- .../util/zip/StreamingZipArchiveFactory.kt | 4 +- .../shared/src/main/res/values/strings.xml | 27 --- .../publication/protection/TestContainer.kt | 8 +- .../util/resource/DirectoryContainerTest.kt | 10 +- .../readium/r2/streamer/ParserAssetFactory.kt | 18 +- .../readium/r2/streamer/PublicationFactory.kt | 52 ++-- .../r2/streamer/container/Container.kt | 1 + .../r2/streamer/parser/PublicationParser.kt | 20 +- .../r2/streamer/parser/audio/AudioParser.kt | 2 +- .../r2/streamer/parser/epub/EpubParser.kt | 6 +- .../r2/streamer/parser/image/ImageParser.kt | 2 +- .../r2/streamer/parser/pdf/PdfParser.kt | 4 +- .../parser/readium/ReadiumWebPubParser.kt | 4 +- .../readium/r2/testapp/domain/CoverStorage.kt | 4 +- .../readium/r2/testapp/domain/ImportError.kt | 4 +- .../r2/testapp/domain/PublicationError.kt | 20 +- .../r2/testapp/domain/PublicationRetriever.kt | 1 - .../r2/testapp/reader/ReaderRepository.kt | 3 +- .../r2/testapp/reader/ReaderViewModel.kt | 4 +- .../r2/testapp/utils/extensions/File.kt | 4 +- 76 files changed, 725 insertions(+), 725 deletions(-) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetError.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/http/{HttpException.kt => HttpError.kt} (52%) diff --git a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt index 9f5bd517cf..99b7606446 100644 --- a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt +++ b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt @@ -6,10 +6,10 @@ package org.readium.adapter.pdfium.document +import com.shockwave.pdfium.PdfDocument as _PdfiumDocument import android.content.Context import android.graphics.Bitmap import android.os.ParcelFileDescriptor -import com.shockwave.pdfium.PdfDocument as _PdfiumDocument import com.shockwave.pdfium.PdfiumCore import java.io.File import kotlin.reflect.KClass @@ -23,7 +23,7 @@ import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry -import org.readium.r2.shared.util.resource.mapCatching +import org.readium.r2.shared.util.resource.decode import org.readium.r2.shared.util.use import timber.log.Timber @@ -104,7 +104,11 @@ public class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory = use { - read().mapCatching { core.fromBytes(it, password) } + read() + .decode( + { core.fromBytes(it, password) }, + { "Pdfium could not read data." } + ) } private fun PdfiumCore.fromFile(file: File, password: String?): PdfiumDocument = diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt index 9e8180272b..5a071fc1a5 100644 --- a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt @@ -40,7 +40,7 @@ public class PdfiumDocumentFragment internal constructor( ) : PdfDocumentFragment() { internal interface Listener { - fun onResourceLoadFailed(href: Url, error: Resource.Exception) + fun onResourceLoadFailed(href: Url, error: Resource.Error) fun onConfigurePdfView(configurator: PDFView.Configurator) fun onTap(point: PointF): Boolean } diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt index d9bd37faa3..cabd2a9304 100644 --- a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt @@ -49,7 +49,7 @@ public class PdfiumEngineProvider( initialPageIndex = input.pageIndex, initialSettings = input.settings, listener = object : PdfiumDocumentFragment.Listener { - override fun onResourceLoadFailed(href: Url, error: Resource.Exception) { + override fun onResourceLoadFailed(href: Url, error: Resource.Error) { input.navigatorListener?.onResourceLoadFailed(href, error) } diff --git a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt index c59201e3e8..7619d8eb12 100644 --- a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt @@ -24,6 +24,7 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.ResourceTry import timber.log.Timber @@ -42,11 +43,13 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory Unit = { Timber.e(it) } + private val onResourceError: (ResourceError) -> Unit = { Timber.e(it) } ) : DataProvider { private val resource = diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt index 6ba6f00b01..3cf9e2f166 100644 --- a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt @@ -69,7 +69,7 @@ public class PsPdfKitDocumentFragment internal constructor( ) : PdfDocumentFragment() { internal interface Listener { - fun onResourceLoadFailed(href: Url, error: Resource.Exception) + fun onResourceLoadFailed(href: Url, error: Resource.Error) fun onConfigurePdfView(builder: PdfConfiguration.Builder): PdfConfiguration.Builder fun onTap(point: PointF): Boolean } diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt index 1f55f6ba95..9ab9e849c1 100644 --- a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt @@ -50,7 +50,7 @@ public class PsPdfKitEngineProvider( initialPageIndex = input.pageIndex, initialSettings = input.settings, listener = object : PsPdfKitDocumentFragment.Listener { - override fun onResourceLoadFailed(href: Url, error: Resource.Exception) { + override fun onResourceLoadFailed(href: Url, error: Resource.Error) { input.navigatorListener?.onResourceLoadFailed(href, error) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index 2be61baff9..86da2dc16e 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -8,7 +8,6 @@ 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.publication.Publication import org.readium.r2.shared.publication.encryption.encryption import org.readium.r2.shared.publication.flatten import org.readium.r2.shared.publication.protection.ContentProtection @@ -17,11 +16,12 @@ 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.asset.Asset +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.asset.AssetType import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.TransformingContainer internal class LcpContentProtection( @@ -42,7 +42,7 @@ internal class LcpContentProtection( asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { return when (asset) { is Asset.Container -> openPublication(asset, credentials, allowUserInteraction) is Asset.Resource -> openLicense(asset, credentials, allowUserInteraction) @@ -53,7 +53,7 @@ internal class LcpContentProtection( asset: Asset.Container, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { val license = retrieveLicense(asset, credentials, allowUserInteraction) return createResultAsset(asset, license) } @@ -73,7 +73,7 @@ internal class LcpContentProtection( private fun createResultAsset( asset: Asset.Container, license: Try - ): Try { + ): Try { val serviceFactory = LcpContentProtectionService .createFactory(license.getOrNull(), license.failureOrNull()) @@ -103,7 +103,7 @@ internal class LcpContentProtection( licenseAsset: Asset.Resource, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction) val licenseDoc = license.getOrNull()?.license @@ -113,7 +113,7 @@ internal class LcpContentProtection( LicenseDocument(it) } catch (e: Exception) { return Try.failure( - Publication.OpenError.InvalidAsset( + AssetError.InvalidAsset( "Failed to read the LCP license document", cause = ThrowableError(e) ) @@ -129,7 +129,7 @@ internal class LcpContentProtection( val link = licenseDoc.publicationLink val url = (link.url() as? AbsoluteUrl) ?: return Try.failure( - Publication.OpenError.InvalidAsset( + AssetError.InvalidAsset( "The LCP license document does not contain a valid link to the publication" ) ) @@ -146,43 +146,43 @@ internal class LcpContentProtection( } else { (assetRetriever.retrieve(url) as? Asset.Container) ?.let { Try.success(it) } - ?: Try.failure(Publication.OpenError.UnsupportedAsset()) + ?: Try.failure(AssetError.UnsupportedAsset()) } return asset.flatMap { createResultAsset(it, license) } } - private fun Resource.Exception.wrap(): Publication.OpenError = + private fun ResourceError.wrap(): AssetError = when (this) { - is Resource.Exception.Forbidden -> - Publication.OpenError.Forbidden(ThrowableError(this)) - is Resource.Exception.NotFound -> - Publication.OpenError.NotFound(ThrowableError(this)) - Resource.Exception.Offline, is Resource.Exception.Unavailable -> - Publication.OpenError.Unavailable(ThrowableError(this)) - is Resource.Exception.Other, is Resource.Exception.BadRequest -> - Publication.OpenError.Unknown(this) - is Resource.Exception.OutOfMemory -> - Publication.OpenError.OutOfMemory(ThrowableError(this)) + is ResourceError.Forbidden -> + AssetError.Forbidden(this) + is ResourceError.NotFound -> + AssetError.NotFound(this) + ResourceError.Offline, is ResourceError.Unavailable -> + AssetError.Unavailable(this) + is ResourceError.Other, is ResourceError.BadRequest -> + AssetError.Unknown(this) + is ResourceError.OutOfMemory -> + AssetError.OutOfMemory(this) } - private fun AssetRetriever.Error.wrap(): Publication.OpenError = + private fun AssetRetriever.Error.wrap(): AssetError = when (this) { is AssetRetriever.Error.ArchiveFormatNotSupported -> - Publication.OpenError.UnsupportedAsset(this) + AssetError.UnsupportedAsset(this) is AssetRetriever.Error.Forbidden -> - Publication.OpenError.Forbidden(this) + AssetError.Forbidden(this) is AssetRetriever.Error.InvalidAsset -> - Publication.OpenError.InvalidAsset(this) + AssetError.InvalidAsset(this) is AssetRetriever.Error.NotFound -> - Publication.OpenError.NotFound(this) + AssetError.NotFound(this) is AssetRetriever.Error.OutOfMemory -> - Publication.OpenError.OutOfMemory(this) + AssetError.OutOfMemory(this) is AssetRetriever.Error.SchemeNotSupported -> - Publication.OpenError.UnsupportedAsset(this) + AssetError.UnsupportedAsset(this) is AssetRetriever.Error.Unavailable -> - Publication.OpenError.Unavailable(this) + AssetError.Unavailable(this) is AssetRetriever.Error.Unknown -> - Publication.OpenError.Unknown(this) + AssetError.Unknown(this) } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 0193a34a09..cae8d292c2 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -22,6 +22,7 @@ import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.FailureResource import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.TransformingResource import org.readium.r2.shared.util.resource.flatMap @@ -51,7 +52,7 @@ internal class LcpDecryptor( } when { - license == null -> FailureResource(Resource.Exception.Forbidden()) + license == null -> FailureResource(ResourceError.Forbidden()) encryption.isDeflated || !encryption.isCbcEncrypted -> FullLcpResource( resource, encryption, diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt index a33cf1918b..eed9ab356a 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt @@ -9,6 +9,7 @@ package org.readium.r2.lcp.license +import java.lang.Error import java.net.HttpURLConnection import java.util.* import kotlinx.coroutines.CancellationException diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt index d7f5856222..d03ea69979 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt @@ -28,7 +28,7 @@ internal class ContainerLicenseContainer( .read() .mapFailure { when (it) { - is Resource.Exception.NotFound -> + is ResourceError.NotFound -> LcpException.Container.FileNotFound(entryUrl) else -> LcpException.Container.ReadFailed(entryUrl) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index 2d555bdab8..9915338c91 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -11,6 +11,7 @@ package org.readium.r2.lcp.service import android.content.Context import java.io.File +import java.lang.Error import kotlin.coroutines.resume import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -35,8 +36,6 @@ import org.readium.r2.shared.extensions.tryOr import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt index c3d674645d..aeb125d4af 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt @@ -59,7 +59,7 @@ public interface Navigator { /** * Called when a publication resource failed to be loaded. */ - public fun onResourceLoadFailed(href: Url, error: Resource.Exception) {} + public fun onResourceLoadFailed(href: Url, error: Resource.Error) {} /** * Called when the navigator jumps to an explicit location, which might break the linear diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt index 6d2a77cc70..27e6f3b329 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt @@ -136,7 +136,7 @@ internal class WebViewServer( } } - private fun errorResource(link: Link, error: Resource.Exception): Resource = + private fun errorResource(link: Link, error: Resource.Error): Resource = StringResource(mediaType = MediaType.XHTML) { withContext(Dispatchers.IO) { Try.success( diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt index 809a74e148..787b40c192 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt @@ -200,17 +200,17 @@ public class ExoMediaPlayer( } override fun onPlayerError(error: PlaybackException) { - var resourceException: Resource.Exception? = error.asInstance() - if (resourceException == null && (error.cause as? HttpDataSource.HttpDataSourceException)?.cause is UnknownHostException) { - resourceException = Resource.Exception.Offline + var resourceError: Resource.Error? = error.asInstance() + if (resourceError == null && (error.cause as? HttpDataSource.HttpDataSourceException)?.cause is UnknownHostException) { + resourceError = Resource.Error.Offline } - if (resourceException != null) { + if (resourceError != null) { player.currentMediaItem?.mediaId ?.let { Url(it) } ?.let { href -> publication.linkWithHref(href) } ?.let { link -> - listener?.onResourceLoadFailed(link, resourceException) + listener?.onResourceLoadFailed(link, resourceError) } } else { Timber.e(error) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt index 92bb181229..0411ef17ba 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt @@ -59,7 +59,7 @@ public interface MediaPlayer { * Called when a resource failed to be loaded, for example because the Internet connection * is offline and the resource is streamed. */ - public fun onResourceLoadFailed(link: Link, error: Resource.Exception) + public fun onResourceLoadFailed(link: Link, error: Resource.Error) /** * Creates the [NotificationMetadata] for the given resource [link]. diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt index 8c43e811b0..dd6a7c8ab1 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt @@ -106,7 +106,7 @@ public open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by * * You should present the exception to the user. */ - public open fun onResourceLoadFailed(link: Link, error: Resource.Exception) { + public open fun onResourceLoadFailed(link: Link, error: Resource.Error) { Toast.makeText(this, error.getUserMessage(this), Toast.LENGTH_LONG).show() } @@ -214,7 +214,7 @@ public open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by this@MediaService.onPlayerStopped() } - override fun onResourceLoadFailed(link: Link, error: Resource.Exception) { + override fun onResourceLoadFailed(link: Link, error: Resource.Error) { this@MediaService.onResourceLoadFailed(link, error) } } diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt index 44a459f5d2..10673c1447 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt @@ -43,6 +43,7 @@ import org.readium.navigator.media.tts.TtsEngine import org.readium.navigator.media.tts.TtsPlayer import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.resource.Resource +import java.lang.Error /** * Adapts the [TtsPlayer] to media3 [Player] interface. @@ -919,31 +920,31 @@ internal class TtsSessionAdapter( private fun TtsPlayer.State.Error.toPlaybackException(): PlaybackException = when (this) { is TtsPlayer.State.Error.EngineError<*> -> mapEngineError(error as E) is TtsPlayer.State.Error.ContentError -> when (exception) { - is Resource.Exception.BadRequest -> + is ResourceError.BadRequest -> PlaybackException(exception.message, exception.cause, ERROR_CODE_IO_BAD_HTTP_STATUS) - is Resource.Exception.NotFound -> + is ResourceError.NotFound -> PlaybackException(exception.message, exception.cause, ERROR_CODE_IO_BAD_HTTP_STATUS) - is Resource.Exception.Forbidden -> + is ResourceError.Forbidden -> PlaybackException( exception.message, exception.cause, ERROR_CODE_DRM_DISALLOWED_OPERATION ) - is Resource.Exception.Unavailable -> + is ResourceError.Unavailable -> PlaybackException( exception.message, exception.cause, ERROR_CODE_IO_NETWORK_CONNECTION_FAILED ) - is Resource.Exception.Offline -> + is ResourceError.Offline -> PlaybackException( exception.message, exception.cause, ERROR_CODE_IO_NETWORK_CONNECTION_FAILED ) - is Resource.Exception.OutOfMemory -> + is ResourceError.OutOfMemory -> PlaybackException(exception.message, exception.cause, ERROR_CODE_UNSPECIFIED) - is Resource.Exception.Other -> + is ResourceError.Other -> PlaybackException(exception.message, exception.cause, ERROR_CODE_UNSPECIFIED) else -> PlaybackException(exception.message, exception.cause, ERROR_CODE_UNSPECIFIED) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index 094ab988af..abf33d33d2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -28,13 +28,12 @@ import org.readium.r2.shared.publication.services.WebPositionsService import org.readium.r2.shared.publication.services.content.ContentService import org.readium.r2.shared.publication.services.search.SearchService import org.readium.r2.shared.util.Closeable -import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.EmptyContainer import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.fallback @@ -209,7 +208,7 @@ public class Publication( return container.get(href) .fallback { error -> - if (error is Resource.Exception.NotFound) { + if (error is ResourceError.NotFound) { // Try again after removing query and fragment. container.get(href.removeQuery().removeFragment()) } else { @@ -493,80 +492,6 @@ public class Publication( } } - /** - * Errors occurring while opening a Publication. - */ - public sealed class OpenError( - override val message: String, - override val cause: Error? = null - ) : Error { - - /** - * The file format could not be recognized by any parser. - */ - public class UnsupportedAsset( - message: String, - cause: Error? - ) : OpenError(message, cause) { - public constructor(message: String) : this(message, null) - public constructor(cause: Error? = null) : this("Asset is not supported.", cause) - } - - /** - * The publication parsing failed with the given underlying error. - */ - public class InvalidAsset( - message: String, - cause: Error? = null - ) : OpenError(message, cause) { - public constructor(cause: Error?) : this( - "The asset seems corrupted so the publication cannot be opened.", - cause - ) - } - - /** - * The publication file was not found on the file system. - */ - public class NotFound(cause: Error? = null) : - OpenError("Asset could not be found.", cause) - - /** - * We're not allowed to open the publication at all, for example because it expired. - */ - public class Forbidden(cause: Error? = null) : - OpenError("You are not allowed to open this publication.", cause) - - /** - * The publication can't be opened at the moment, for example because of a networking error. - * This error is generally temporary, so the operation may be retried or postponed. - */ - public class Unavailable(cause: Error? = null) : - OpenError("The publication is not available at the moment.", cause) - - /** - * The provided credentials are incorrect and we can't open the publication in a - * `restricted` state (e.g. for a password-protected ZIP). - */ - public class IncorrectCredentials(cause: Error? = null) : - OpenError("Provided credentials were incorrect.", cause) - - /** - * Opening the publication exceeded the available device memory. - */ - public class OutOfMemory(cause: Error? = null) : - OpenError("There is not enough memory available to open the publication.", cause) - - /** - * An unexpected error occurred. - */ - public class Unknown(cause: Error? = null) : - OpenError("An unexpected error occurred.", cause) { - - public constructor(exception: Exception) : this(ThrowableError(exception)) - } - } - /** * Builds a Publication from its components. * @@ -630,10 +555,10 @@ public class Publication( level = DeprecationLevel.ERROR ) @Suppress("UNUSED_PARAMETER") - public fun resource(href: String): Link? = throw NotImplementedError() + public fun resource(href: String): Link = throw NotImplementedError() @Deprecated("Refactored as a property", ReplaceWith("baseUrl"), level = DeprecationLevel.ERROR) - public fun baseUrl(): URL? = throw NotImplementedError() + public fun baseUrl(): URL = throw NotImplementedError() @Deprecated( "Renamed [subcollections]", @@ -658,7 +583,7 @@ public class Publication( level = DeprecationLevel.ERROR ) @Suppress("UNUSED_PARAMETER") - public fun resourceWithHref(href: String): Link? = throw NotImplementedError() + public fun resourceWithHref(href: String): Link = throw NotImplementedError() @Deprecated( "Use a [ServiceFactory] for a [PositionsService] instead.", diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt index ee925b4e97..2b20a07390 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt @@ -7,7 +7,7 @@ package org.readium.r2.shared.publication.protection import org.readium.r2.shared.InternalReadiumApi -import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.publication.protection.ContentProtection.Scheme import org.readium.r2.shared.publication.services.contentProtectionServiceFactory import org.readium.r2.shared.util.Try @@ -27,8 +27,8 @@ public class AdeptFallbackContentProtection : ContentProtection { override val scheme: Scheme = Scheme.Adept - override suspend fun supports(asset: org.readium.r2.shared.util.asset.Asset): Boolean { - if (asset !is org.readium.r2.shared.util.asset.Asset.Container) { + override suspend fun supports(asset: Asset): Boolean { + if (asset !is Asset.Container) { return false } @@ -36,13 +36,13 @@ public class AdeptFallbackContentProtection : ContentProtection { } override suspend fun open( - asset: org.readium.r2.shared.util.asset.Asset, + asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try { - if (asset !is org.readium.r2.shared.util.asset.Asset.Container) { + ): Try { + if (asset !is Asset.Container) { return Try.failure( - Publication.OpenError.UnsupportedAsset("A container asset was expected.") + AssetError.UnsupportedAsset("A container asset was expected.") ) } @@ -58,7 +58,7 @@ public class AdeptFallbackContentProtection : ContentProtection { return Try.success(protectedFile) } - private suspend fun isAdept(asset: org.readium.r2.shared.util.asset.Asset.Container): Boolean { + private suspend fun isAdept(asset: Asset.Container): Boolean { if (!asset.mediaType.matches(MediaType.EPUB)) { return false } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index a02f8d9605..2d17e67ce2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -12,6 +12,7 @@ package org.readium.r2.shared.publication.protection import androidx.annotation.StringRes import org.readium.r2.shared.R import org.readium.r2.shared.UserException +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.publication.LocalizedString import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.ContentProtectionService @@ -47,7 +48,7 @@ public interface ContentProtection { asset: org.readium.r2.shared.util.asset.Asset, credentials: String?, allowUserInteraction: Boolean - ): Try + ): Try /** * Holds the result of opening an [Asset] with a [ContentProtection]. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index 4ecedf082b..435ff90ecf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -8,8 +8,8 @@ package org.readium.r2.shared.publication.protection import org.json.JSONObject import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.publication.Manifest -import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.encryption.encryption import org.readium.r2.shared.publication.protection.ContentProtection.Scheme import org.readium.r2.shared.publication.services.contentProtectionServiceFactory @@ -36,25 +36,25 @@ public class LcpFallbackContentProtection( override val scheme: Scheme = Scheme.Lcp - override suspend fun supports(asset: org.readium.r2.shared.util.asset.Asset): Boolean = + override suspend fun supports(asset: Asset): Boolean = when (asset) { - is org.readium.r2.shared.util.asset.Asset.Container -> isLcpProtected( + is Asset.Container -> isLcpProtected( asset.container, asset.mediaType ) - is org.readium.r2.shared.util.asset.Asset.Resource -> asset.mediaType.matches( + is Asset.Resource -> asset.mediaType.matches( MediaType.LCP_LICENSE_DOCUMENT ) } override suspend fun open( - asset: org.readium.r2.shared.util.asset.Asset, + asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try { - if (asset !is org.readium.r2.shared.util.asset.Asset.Container) { + ): Try { + if (asset !is Asset.Container) { return Try.failure( - Publication.OpenError.UnsupportedAsset("A container asset was expected.") + AssetError.UnsupportedAsset("A container asset was expected.") ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt index 041bd37e8a..11de34effb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt @@ -26,6 +26,7 @@ import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.FailureResource import org.readium.r2.shared.util.resource.LazyResource import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.StringResource /** @@ -300,13 +301,13 @@ private sealed class RouteHandler { val query = url.query val text = query.firstNamedOrNull("text") ?: return FailureResource( - Resource.Exception.BadRequest( + ResourceError.BadRequest( IllegalArgumentException("'text' parameter is required") ) ) val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() ?: return FailureResource( - Resource.Exception.BadRequest( + ResourceError.BadRequest( IllegalArgumentException("if present, 'peek' must be true or false") ) ) @@ -314,7 +315,7 @@ private sealed class RouteHandler { val copyAllowed = with(service.rights) { if (peek) canCopy(text) else copy(text) } return if (!copyAllowed) { - FailureResource(Resource.Exception.Forbidden()) + FailureResource(ResourceError.Forbidden()) } else { StringResource("true", MediaType.JSON) } @@ -340,20 +341,20 @@ private sealed class RouteHandler { val query = url.query val pageCountString = query.firstNamedOrNull("pageCount") ?: return FailureResource( - Resource.Exception.BadRequest( + ResourceError.BadRequest( IllegalArgumentException("'pageCount' parameter is required") ) ) val pageCount = pageCountString.toIntOrNull()?.takeIf { it >= 0 } ?: return FailureResource( - Resource.Exception.BadRequest( + ResourceError.BadRequest( IllegalArgumentException("'pageCount' must be a positive integer") ) ) val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() ?: return FailureResource( - Resource.Exception.BadRequest( + ResourceError.BadRequest( IllegalArgumentException("if present, 'peek' must be true or false") ) ) @@ -369,7 +370,7 @@ private sealed class RouteHandler { } return if (!printAllowed) { - FailureResource(Resource.Exception.Forbidden()) + FailureResource(ResourceError.Forbidden()) } else { StringResource("true", mediaType = MediaType.JSON) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index e530bf5915..edc7817fe1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -17,12 +17,14 @@ import org.readium.r2.shared.extensions.toPng import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.ServiceFactory +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.BytesResource import org.readium.r2.shared.util.resource.FailureResource import org.readium.r2.shared.util.resource.LazyResource import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError /** * Provides an easy access to a bitmap version of the publication cover. @@ -110,8 +112,11 @@ public abstract class GeneratedCoverService : CoverService { val png = cover.toPng() if (png == null) { - val error = Exception("Unable to convert cover to PNG.") - FailureResource(error) + FailureResource( + ResourceError.InvalidContent( + MessageError("Unable to convert cover to PNG.") + ) + ) } else { BytesResource(png, mediaType = MediaType.PNG) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt index a46ee6bf12..7aaa5fccc7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt @@ -38,6 +38,7 @@ import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.readAsString import org.readium.r2.shared.util.use +import org.readium.r2.shared.util.w import timber.log.Timber /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt index 9a157fefbd..c67d4ce6f2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt @@ -7,84 +7,63 @@ package org.readium.r2.shared.publication.services.search import android.os.Parcelable -import androidx.annotation.StringRes import kotlinx.parcelize.Parcelize import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.R -import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.LocatorCollection import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.ServiceFactory +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.http.HttpException -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.http.HttpError @ExperimentalReadiumApi -public typealias SearchTry = Try +public typealias SearchTry = Try /** * Represents an error which might occur during a search activity. */ @ExperimentalReadiumApi -public sealed class SearchException(content: Content, cause: Throwable? = null) : UserException( - content, - cause -) { - protected constructor(@StringRes userMessageId: Int, vararg args: Any, cause: Throwable? = null) : - this(Content(userMessageId, *args), cause) - protected constructor(cause: UserException) : - this(Content(cause), cause) +public sealed class SearchError( + override val message: String, + override val cause: Error? = null +) : Error { /** * The publication is not searchable. */ - public object PublicationNotSearchable : SearchException( - R.string.readium_shared_search_exception_publication_not_searchable - ) + public object PublicationNotSearchable + : SearchError("This publication is not searchable.") /** * The provided search query cannot be handled by the service. */ - public class BadQuery(cause: UserException) : SearchException(cause) + public class BadQuery(cause: Error) + : SearchError("The provided search query cannot be handled by the service.", cause) /** * An error occurred while accessing one of the publication's resources. */ - public class ResourceError(cause: Resource.Exception) : SearchException(cause) + public class ResourceError(cause: Error) + : SearchError("An error occurred while accessing one of the publication's resources.", cause) /** * An error occurred while performing an HTTP request. */ - public class NetworkError(cause: HttpException) : SearchException(cause) + public class NetworkError(cause: HttpError) + : SearchError("An error occurred while performing an HTTP request.", cause) /** * The search was cancelled by the caller. * * For example, when a coroutine or a network request is cancelled. */ - public object Cancelled : SearchException(R.string.readium_shared_search_exception_cancelled) + public object Cancelled + : SearchError("The search was cancelled.") /** For any other custom service error. */ - public class Other(cause: Throwable) : SearchException( - R.string.readium_shared_search_exception_other, - cause = cause - ) - - public companion object { - public fun wrap(e: Throwable): SearchException = - when (e) { - is SearchException -> e - is Resource.Exception -> ResourceError(e) - is HttpException -> - if (e.kind == HttpException.Kind.Cancelled) { - Cancelled - } else { - NetworkError(e) - } - else -> Other(e) - } - } + public class Other(cause: Error) + : SearchError("An error occurred while searching.", cause) } /** @@ -163,7 +142,7 @@ public val Publication.searchOptions: SearchService.Options get() = @ExperimentalReadiumApi public suspend fun Publication.search(query: String, options: SearchService.Options? = null): SearchTry = findService(SearchService::class)?.search(query, options) - ?: Try.failure(SearchException.PublicationNotSearchable) + ?: Try.failure(SearchError.PublicationNotSearchable) /** Factory to build a [SearchService] */ @ExperimentalReadiumApi diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt index b86e2b401d..f549e87bad 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt @@ -20,9 +20,10 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.* import org.readium.r2.shared.publication.services.positionsByReadingOrder import org.readium.r2.shared.publication.services.search.SearchService.Options +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.getOrThrow +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.content.DefaultResourceContentExtractorFactory import org.readium.r2.shared.util.resource.content.ResourceContentExtractor @@ -86,7 +87,7 @@ public class StringSearchService( ) ) } catch (e: Exception) { - Try.failure(SearchException.wrap(e)) + Try.failure(SearchError.Other(ThrowableError(e))) } private inner class Iterator( @@ -116,7 +117,9 @@ public class StringSearchService( val link = manifest.readingOrder[index] val text = container.get(link.url()) - .let { extractorFactory.createExtractor(it)?.extractText(it)?.getOrThrow() } + .let { extractorFactory.createExtractor(it)?.extractText(it) } + ?.getOrElse { return Try.failure(SearchError.ResourceError(it)) } + if (text == null) { Timber.w("Cannot extract text from resource: ${link.href}") return next() @@ -133,7 +136,7 @@ public class StringSearchService( return Try.success(LocatorCollection(locators = locators)) } catch (e: Exception) { - return Try.failure(SearchException.wrap(e)) + return Try.failure(SearchError.Other(ThrowableError(e))) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt index ddb6775f11..59d4775bd6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt @@ -6,6 +6,9 @@ package org.readium.r2.shared.util +import org.readium.r2.shared.InternalReadiumApi +import timber.log.Timber + /** * Describes an error. */ @@ -33,8 +36,8 @@ public class MessageError( /** * An error caused by the catch of a throwable. */ -public class ThrowableError( - public val throwable: Throwable +public class ThrowableError( + public val throwable: E ) : Error { override val message: String = throwable.message ?: throwable.toString() override val cause: Error? = null @@ -46,3 +49,20 @@ public class ThrowableError( public class ErrorException( public val error: Error ) : Exception(error.message) + +public fun Try.getOrThrow(): S = + when (this) { + is Try.Success -> value + is Try.Failure -> throw Exception("Try was excepted to contain a success.") + } + +//FIXME: to improve +@InternalReadiumApi +public fun Timber.Forest.e(error: Error, message: String? = null) { + Timber.e(Exception(error.message), message) +} + +@InternalReadiumApi +public fun Timber.Forest.w(error: Error, message: String? = null) { + Timber.w(Exception(error.message), message) +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetError.kt new file mode 100644 index 0000000000..dbfc81e435 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetError.kt @@ -0,0 +1,78 @@ +package org.readium.r2.shared.util.asset + +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ThrowableError + +/** + * Errors occurring while opening a Publication. + */ +public sealed class AssetError( + override val message: String, + override val cause: Error? = null +) : Error { + + /** + * The file format could not be recognized by any parser. + */ + public class UnsupportedAsset( + message: String, + cause: Error? + ) : AssetError(message, cause) { + public constructor(message: String) : this(message, null) + public constructor(cause: Error? = null) : this("Asset is not supported.", cause) + } + + /** + * The publication parsing failed with the given underlying error. + */ + public class InvalidAsset( + message: String, + cause: Error? = null + ) : AssetError(message, cause) { + public constructor(cause: Error?) : this( + "The asset seems corrupted so the publication cannot be opened.", + cause + ) + } + + /** + * The publication file was not found on the file system. + */ + public class NotFound(cause: Error? = null) : + AssetError("Asset could not be found.", cause) + + /** + * We're not allowed to open the publication at all, for example because it expired. + */ + public class Forbidden(cause: Error? = null) : + AssetError("You are not allowed to open this publication.", cause) + + /** + * The publication can't be opened at the moment, for example because of a networking error. + * This error is generally temporary, so the operation may be retried or postponed. + */ + public class Unavailable(cause: Error? = null) : + AssetError("The publication is not available at the moment.", cause) + + /** + * The provided credentials are incorrect and we can't open the publication in a + * `restricted` state (e.g. for a password-protected ZIP). + */ + public class IncorrectCredentials(cause: Error? = null) : + AssetError("Provided credentials were incorrect.", cause) + + /** + * Opening the publication exceeded the available device memory. + */ + public class OutOfMemory(cause: Error? = null) : + AssetError("There is not enough memory available to open the publication.", cause) + + /** + * An unexpected error occurred. + */ + public class Unknown(cause: Error? = null) : + AssetError("An unexpected error occurred.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } +} \ No newline at end of file diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 48dc41786f..71e7167e44 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -36,6 +36,7 @@ import org.readium.r2.shared.util.resource.DirectoryContainerFactory import org.readium.r2.shared.util.resource.FileResourceFactory import org.readium.r2.shared.util.resource.FileZipArchiveFactory import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.ResourceFactory import org.readium.r2.shared.util.resource.ResourceMediaTypeSnifferContent import org.readium.r2.shared.util.toUrl @@ -124,8 +125,8 @@ public class AssetRetriever( ThrowableError(error) ) - public class Unknown(exception: Exception) : - Error("Something unexpected happened.", ThrowableError(exception)) + public class Unknown(error: SharedError) : + Error("Something unexpected happened.", error) } /** @@ -162,7 +163,7 @@ public class AssetRetriever( error ) is ArchiveFactory.Error.ResourceReading -> - error.resourceException.wrap(url) + error.cause.wrap(url) is ArchiveFactory.Error.PasswordsNotSupported -> Error.ArchiveFormatNotSupported( error @@ -231,26 +232,26 @@ public class AssetRetriever( } } - private fun Resource.Exception.wrap(url: AbsoluteUrl): Error = + private fun ResourceError.wrap(url: AbsoluteUrl): Error = when (this) { - is Resource.Exception.Forbidden -> + is ResourceError.Forbidden -> Error.Forbidden( url, this ) - is Resource.Exception.NotFound -> + is ResourceError.NotFound -> Error.InvalidAsset( this ) - is Resource.Exception.Unavailable, Resource.Exception.Offline -> + is ResourceError.Unavailable, ResourceError.Offline -> Error.Unavailable( this ) - is Resource.Exception.OutOfMemory -> + is ResourceError.OutOfMemory -> Error.OutOfMemory( - cause + cause.throwable ) - is Resource.Exception.Other -> + is ResourceError.Other -> Error.Unknown( this ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt index 6218c48ab5..0444588d40 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt @@ -41,21 +41,9 @@ public interface DownloadManager { override val cause: org.readium.r2.shared.util.Error? = null ) : org.readium.r2.shared.util.Error { - public class NotFound( - cause: org.readium.r2.shared.util.Error? = null - ) : Error("File not found.", cause) - - public class Unreachable( - cause: org.readium.r2.shared.util.Error? = null - ) : Error("Server is not reachable.", cause) - - public class Server( - cause: org.readium.r2.shared.util.Error? = null - ) : Error("An error occurred on the server-side.", cause) - - public class Forbidden( - cause: org.readium.r2.shared.util.Error? = null - ) : Error("Access to the resource was denied.", cause) + public class HttpError( + cause: org.readium.r2.shared.util.http.HttpError + ) : Error(cause.message, cause) public class DeviceNotFound( cause: org.readium.r2.shared.util.Error? = null @@ -69,14 +57,9 @@ public interface DownloadManager { cause: org.readium.r2.shared.util.Error? = null ) : Error("There is not enough space to complete the download.", cause) - public class FileError( - message: String, - cause: org.readium.r2.shared.util.Error? = null - ) : Error(message, cause) - - public class HttpData( + public class FileSystemError( cause: org.readium.r2.shared.util.Error? = null - ) : Error("A data error occurred at the HTTP level.", cause) + ) : Error("IO error on the local device.", cause) public class TooManyRedirects( cause: org.readium.r2.shared.util.Error? = null diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index 9851bf812e..88585b1568 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -24,8 +24,11 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.tryOr +import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever @@ -288,41 +291,39 @@ public class AndroidDownloadManager internal constructor( Try.success(download) } else { Try.failure( - DownloadManager.Error.FileError("Failed to rename the downloaded file.") + DownloadManager.Error.FileSystemError( + MessageError("Failed to rename the downloaded file.")) ) } } private fun mapErrorCode(code: Int): DownloadManager.Error = when (code) { - 401, 403 -> - DownloadManager.Error.Forbidden() - 404 -> - DownloadManager.Error.NotFound() - 500, 501 -> - DownloadManager.Error.Server() - 502, 503, 504 -> - DownloadManager.Error.Unreachable() + in 400 until 1000 -> + DownloadManager.Error.HttpError(httpErrorForCode(code)) + SystemDownloadManager.ERROR_UNHANDLED_HTTP_CODE -> + DownloadManager.Error.HttpError(httpErrorForCode(code)) + SystemDownloadManager.ERROR_HTTP_DATA_ERROR -> + DownloadManager.Error.HttpError(HttpError(HttpError.Kind.Other)) + SystemDownloadManager.ERROR_TOO_MANY_REDIRECTS -> + DownloadManager.Error.HttpError(HttpError(HttpError.Kind.TooManyRedirects)) SystemDownloadManager.ERROR_CANNOT_RESUME -> DownloadManager.Error.CannotResume() SystemDownloadManager.ERROR_DEVICE_NOT_FOUND -> DownloadManager.Error.DeviceNotFound() SystemDownloadManager.ERROR_FILE_ERROR -> - DownloadManager.Error.FileError("IO error on the local device.") - SystemDownloadManager.ERROR_HTTP_DATA_ERROR -> - DownloadManager.Error.HttpData() + DownloadManager.Error.FileSystemError() SystemDownloadManager.ERROR_INSUFFICIENT_SPACE -> DownloadManager.Error.InsufficientSpace() - SystemDownloadManager.ERROR_TOO_MANY_REDIRECTS -> - DownloadManager.Error.TooManyRedirects() - SystemDownloadManager.ERROR_UNHANDLED_HTTP_CODE -> - DownloadManager.Error.Unknown() SystemDownloadManager.ERROR_UNKNOWN -> DownloadManager.Error.Unknown() else -> DownloadManager.Error.Unknown() } + private fun httpErrorForCode(code: Int): HttpError = + HttpError(code) ?: HttpError(HttpError.Kind.Other) + public override fun close() { listeners.clear() coroutineScope.cancel() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt index f8817a71e0..92963ccfcf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt @@ -22,7 +22,7 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpException +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpResponse import org.readium.r2.shared.util.http.HttpTry @@ -87,7 +87,7 @@ public class ForegroundDownloadManager( } .onFailure { error -> forEachListener(id) { - onDownloadFailed(id, mapError(error)) + onDownloadFailed(id, DownloadManager.Error.HttpError(error)) } } @@ -148,50 +148,6 @@ public class ForegroundDownloadManager( } } } catch (e: Exception) { - Try.failure(HttpException.wrap(e)) + Try.failure(HttpError(HttpError.Kind.Other, cause = ThrowableError(e))) } - - private fun mapError(httpException: HttpException): DownloadManager.Error { - val httpError = ThrowableError(httpException) - return when (httpException.kind) { - HttpException.Kind.MalformedRequest -> - DownloadManager.Error.Unknown(httpError) - - HttpException.Kind.MalformedResponse -> - DownloadManager.Error.HttpData(httpError) - - HttpException.Kind.Timeout -> - DownloadManager.Error.Unreachable(httpError) - - HttpException.Kind.BadRequest -> - DownloadManager.Error.Unknown(httpError) - - HttpException.Kind.Unauthorized -> - DownloadManager.Error.Forbidden(httpError) - - HttpException.Kind.Forbidden -> - DownloadManager.Error.Forbidden(httpError) - - HttpException.Kind.MethodNotAllowed -> - DownloadManager.Error.Unknown(httpError) - - HttpException.Kind.NotFound -> - DownloadManager.Error.NotFound(httpError) - - HttpException.Kind.ClientError -> - DownloadManager.Error.HttpData(httpError) - - HttpException.Kind.ServerError -> - DownloadManager.Error.Server(httpError) - - HttpException.Kind.Offline -> - DownloadManager.Error.Unreachable(httpError) - - HttpException.Kind.Cancelled -> - DownloadManager.Error.Unknown(httpError) - - HttpException.Kind.Other -> - DownloadManager.Error.Unknown(httpError) - } - } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index 1a03b2afca..21fd81399b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -11,13 +11,18 @@ import java.io.ByteArrayInputStream import java.io.FileInputStream import java.io.InputStream import java.net.HttpURLConnection +import java.net.MalformedURLException +import java.net.SocketTimeoutException import java.net.URL +import java.util.concurrent.CancellationException import kotlin.time.Duration import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.joinValues import org.readium.r2.shared.extensions.lowerCaseKeys +import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.e import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.http.HttpRequest.Method import org.readium.r2.shared.util.mediatype.BytesResourceMediaTypeSnifferContent @@ -93,9 +98,9 @@ public class DefaultHttpClient( * You can return either: * - a new recovery request to start * - the [error] argument, if you cannot recover from it - * - a new [HttpException] to provide additional information + * - a new [HttpError] to provide additional information */ - public suspend fun onRecoverRequest(request: HttpRequest, error: HttpException): HttpTry = + public suspend fun onRecoverRequest(request: HttpRequest, error: HttpError): HttpTry = Try.failure(error) /** @@ -109,14 +114,14 @@ public class DefaultHttpClient( * You can return either: * - the provided [newRequest] to proceed with the redirection * - a different redirection request - * - a [HttpException.CANCELLED] error to abort the redirection + * - a [HttpError.CANCELLED] error to abort the redirection */ public suspend fun onFollowUnsafeRedirect( request: HttpRequest, response: HttpResponse, newRequest: HttpRequest ): HttpTry = - Try.failure(HttpException.CANCELLED) + Try.failure(HttpError.CANCELLED) /** * Called when the HTTP client received an HTTP response for the given [request]. @@ -135,7 +140,7 @@ public class DefaultHttpClient( * * This will be called only if [onRecoverRequest] is not implemented, or returns an error. */ - public suspend fun onRequestFailed(request: HttpRequest, error: HttpException) {} + public suspend fun onRequestFailed(request: HttpRequest, error: HttpError) {} } // We are using Dispatchers.IO but we still get this warning... @@ -148,7 +153,7 @@ public class DefaultHttpClient( var connection = request.toHttpURLConnection() val statusCode = connection.responseCode - HttpException.Kind.ofStatusCode(statusCode)?.let { kind -> + HttpError.Kind.ofStatusCode(statusCode)?.let { kind -> // It was a HEAD request? We need to query the resource again to get the error body. // The body is needed for example when the response is an OPDS Authentication // Document. @@ -169,7 +174,7 @@ public class DefaultHttpClient( content = BytesResourceMediaTypeSnifferContent { it } ) } - throw HttpException(kind, mediaType, body) + return@withContext Try.failure(HttpError(kind, mediaType, body)) } val mediaType = mediaTypeRetriever.retrieve(MediaTypeHints(connection)) @@ -195,14 +200,14 @@ public class DefaultHttpClient( ) } } catch (e: Exception) { - Try.failure(HttpException.wrap(e)) + Try.failure(wrap(e)) } } return callback.onStartRequest(request) .flatMap { tryStream(it) } .tryRecover { error -> - if (error.kind != HttpException.Kind.Cancelled) { + if (error.kind != HttpError.Kind.Cancelled) { callback.onRecoverRequest(request, error) .flatMap { stream(it) } } else { @@ -230,11 +235,11 @@ public class DefaultHttpClient( // > https://www.rfc-editor.org/rfc/rfc1945.html#section-9.3 val redirectCount = request.extras.getInt(EXTRA_REDIRECT_COUNT) if (redirectCount > 5) { - return Try.failure(HttpException.CANCELLED) + return Try.failure(HttpError(HttpError.Kind.TooManyRedirects)) } val location = response.header("Location") - ?: return Try.failure(HttpException(kind = HttpException.Kind.MalformedResponse)) + ?: return Try.failure(HttpError(kind = HttpError.Kind.MalformedResponse)) val newRequest = HttpRequest( url = location, @@ -309,6 +314,20 @@ public class DefaultHttpClient( } } +/** + * Creates an HTTP error from a generic exception. + */ +private fun wrap(cause: Throwable): HttpError { + val kind = when (cause) { + is MalformedURLException -> HttpError.Kind.MalformedRequest + is CancellationException -> HttpError.Kind.Cancelled + is SocketTimeoutException -> HttpError.Kind.Timeout + else -> HttpError.Kind.Other + } + + return HttpError(kind = kind, cause = ThrowableError(cause)) +} + /** * [HttpURLConnection]'s input stream which disconnects when closed. */ diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt index 9581d2d1de..77da2aaed6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.mediatype.MediaType @@ -125,9 +126,13 @@ public suspend fun HttpClient.fetch(request: HttpRequest): HttpTry HttpClient.fetchWithDecoder( fetch(request) .flatMap { try { - Try.success(decoder(it)) + Try.success( + decoder(it) + ) } catch (e: Exception) { - Try.failure(HttpException(kind = HttpException.Kind.MalformedResponse, cause = e)) + Try.failure( + HttpError(kind = HttpError.Kind.MalformedResponse, cause = ThrowableError(e)) + ) } } @@ -192,7 +201,7 @@ public suspend fun HttpClient.head(request: HttpRequest): HttpTry .copy { method = HttpRequest.Method.HEAD } .response() .tryRecover { exception -> - if (exception.kind != HttpException.Kind.MethodNotAllowed) { + if (exception.kind != HttpError.Kind.MethodNotAllowed) { return@tryRecover Try.failure(exception) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt index 10f7e1d62e..b51c48c0a8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt @@ -10,7 +10,7 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.FailureResource -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.toEntry /** @@ -34,7 +34,7 @@ public class HttpContainer( return if (absoluteUrl == null || !absoluteUrl.isHttp) { FailureResource( - Resource.Exception.NotFound( + ResourceError.NotFound( Exception("URL scheme is not supported: ${absoluteUrl?.scheme}.") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpException.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt similarity index 52% rename from readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpException.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt index 28dd2f393d..2c89783d21 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpException.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt @@ -6,19 +6,13 @@ package org.readium.r2.shared.util.http -import android.content.Context -import androidx.annotation.StringRes -import java.net.MalformedURLException -import java.net.SocketTimeoutException -import java.util.concurrent.CancellationException import org.json.JSONObject -import org.readium.r2.shared.R -import org.readium.r2.shared.UserException import org.readium.r2.shared.extensions.tryOrLog +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.mediatype.MediaType -public typealias HttpTry = Try +public typealias HttpTry = Try /** * Represents an error occurring during an HTTP activity. @@ -28,52 +22,55 @@ public typealias HttpTry = Try * @param body Response body. * @param cause Underlying error, if any. */ -public class HttpException( +public class HttpError( public val kind: Kind, public val mediaType: MediaType? = null, public val body: ByteArray? = null, - cause: Throwable? = null -) : UserException(kind.userMessageId, cause = cause) { + public override val cause: Error? = null +) : Error { - public enum class Kind(@StringRes public val userMessageId: Int) { + public enum class Kind(public val message: String) { /** The provided request was not valid. */ - MalformedRequest(R.string.readium_shared_http_exception_malformed_request), + MalformedRequest("The provided request was not valid."), /** The received response couldn't be decoded. */ - MalformedResponse(R.string.readium_shared_http_exception_malformed_response), + MalformedResponse("The received response could not be decoded."), /** The client, server or gateways timed out. */ - Timeout(R.string.readium_shared_http_exception_timeout), + Timeout("Request timed out."), /** (400) The server cannot or will not process the request due to an apparent client error. */ - BadRequest(R.string.readium_shared_http_exception_bad_request), + BadRequest("The provided request was not valid."), /** (401) Authentication is required and has failed or has not yet been provided. */ - Unauthorized(R.string.readium_shared_http_exception_unauthorized), + Unauthorized("Authentication required."), /** (403) The server refuses the action, probably because we don't have the necessary permissions. */ - Forbidden(R.string.readium_shared_http_exception_forbidden), + Forbidden("You are not authorized."), /** (404) The requested resource could not be found. */ - NotFound(R.string.readium_shared_http_exception_not_found), + NotFound("Page not found."), /** (405) Method not allowed. */ - MethodNotAllowed(R.string.readium_shared_http_exception_method_not_allowed), + MethodNotAllowed("Method not allowed."), /** (4xx) Other client errors */ - ClientError(R.string.readium_shared_http_exception_client_error), + ClientError("A client error occurred."), /** (5xx) Server errors */ - ServerError(R.string.readium_shared_http_exception_server_error), + ServerError("A server error occurred, please try again later."), /** The device is offline. */ - Offline(R.string.readium_shared_http_exception_offline), + Offline("Your Internet connection appears to be offline."), + + /** Too many redirects */ + TooManyRedirects("There were too many redirects to follow."), /** The request was cancelled. */ - Cancelled(R.string.readium_shared_http_exception_cancelled), + Cancelled("The request was cancelled."), /** An error whose kind is not recognized. */ - Other(R.string.readium_shared_http_exception_other); + Other("A networking error occurred."); public companion object { @@ -94,25 +91,8 @@ public class HttpException( } } - override fun getUserMessage(context: Context, includesCauses: Boolean): String { - problemDetails?.let { error -> - var message = error.title - if (error.detail != null) { - message += "\n" + error.detail - } - return message - } - - return super.getUserMessage(context, includesCauses) - } - - override fun getLocalizedMessage(): String { - var message = "HTTP error: ${kind.name}" - problemDetails?.let { details -> - message += ": ${details.title} ${details.detail}" - } - return message - } + override val message: String + get() = kind.message /** Response body parsed as a JSON problem details. */ public val problemDetails: ProblemDetails? by lazy { @@ -128,7 +108,7 @@ public class HttpException( /** * Shortcut for a cancelled HTTP error. */ - public val CANCELLED: HttpException = HttpException(kind = Kind.Cancelled) + public val CANCELLED: HttpError = HttpError(kind = Kind.Cancelled) /** * Creates an HTTP error from a status code. @@ -139,24 +119,9 @@ public class HttpException( statusCode: Int, mediaType: MediaType? = null, body: ByteArray? = null - ): HttpException? = + ): HttpError? = Kind.ofStatusCode(statusCode)?.let { kind -> - HttpException(kind, mediaType, body) + HttpError(kind, mediaType, body) } - - /** - * Creates an HTTP error from a generic exception. - */ - public fun wrap(cause: Throwable): HttpException { - val kind = when (cause) { - is HttpException -> return cause - is MalformedURLException -> Kind.MalformedRequest - is CancellationException -> Kind.Cancelled - is SocketTimeoutException -> Kind.Timeout - else -> Kind.Other - } - - return HttpException(kind = kind, cause = cause) - } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index 6ccc93be4d..c788678184 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -7,11 +7,13 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.extensions.read import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.io.CountingInputStream import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.ResourceTry /** Provides access to an external URL. */ @@ -34,7 +36,7 @@ public class HttpResource( return if (contentLength != null) { Try.success(contentLength) } else { - Try.failure(Resource.Exception.Unavailable()) + Try.failure(ResourceError.Unavailable()) } } @@ -51,10 +53,8 @@ public class HttpResource( stream.readBytes() } } - } catch (e: HttpException) { - Try.failure(Resource.Exception.wrapHttp(e)) } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(e)) + Try.failure(ResourceError.Other(e)) } } @@ -67,7 +67,7 @@ public class HttpResource( } _headResponse = client.head(HttpRequest(source.toString())) - .mapFailure { Resource.Exception.wrapHttp(it) } + .mapFailure { ResourceError.wrapHttp(it) } return _headResponse } @@ -99,14 +99,14 @@ public class HttpResource( .flatMap { response -> if (from != null && response.response.statusCode != 206 ) { - val exception = Exception("Server seems not to support range requests.") - Try.failure(HttpException.wrap(exception)) + val error = MessageError("Server seems not to support range requests.") + Try.failure(HttpError(HttpError.Kind.Other, cause = error)) } else { Try.success(response) } } .map { CountingInputStream(it.body) } - .mapFailure { Resource.Exception.wrapHttp(it) } + .mapFailure { ResourceError.wrapHttp(it) } .onSuccess { inputStream = it inputStreamStart = from ?: 0 @@ -116,20 +116,20 @@ public class HttpResource( private var inputStream: CountingInputStream? = null private var inputStreamStart = 0L - private fun Resource.Exception.Companion.wrapHttp(e: HttpException): Resource.Exception = + private fun ResourceError.Companion.wrapHttp(e: HttpError): ResourceError = when (e.kind) { - HttpException.Kind.MalformedRequest, HttpException.Kind.BadRequest, HttpException.Kind.MethodNotAllowed -> - Resource.Exception.BadRequest(cause = e) - HttpException.Kind.Timeout, HttpException.Kind.Offline -> - Resource.Exception.Unavailable(e) - HttpException.Kind.Unauthorized, HttpException.Kind.Forbidden -> - Resource.Exception.Forbidden(e) - HttpException.Kind.NotFound -> - Resource.Exception.NotFound(e) - HttpException.Kind.Cancelled -> - Resource.Exception.Unavailable(e) - HttpException.Kind.MalformedResponse, HttpException.Kind.ClientError, HttpException.Kind.ServerError, HttpException.Kind.Other -> - Resource.Exception.Other(e) + HttpError.Kind.MalformedRequest, HttpError.Kind.BadRequest, HttpError.Kind.MethodNotAllowed -> + ResourceError.BadRequest(cause = e) + HttpError.Kind.Timeout, HttpError.Kind.Offline, HttpError.Kind.TooManyRedirects -> + ResourceError.Unavailable(e) + HttpError.Kind.Unauthorized, HttpError.Kind.Forbidden -> + ResourceError.Forbidden(e) + HttpError.Kind.NotFound -> + ResourceError.NotFound(e) + HttpError.Kind.Cancelled -> + ResourceError.Unavailable(e) + HttpError.Kind.MalformedResponse, HttpError.Kind.ClientError, HttpError.Kind.ServerError, HttpError.Kind.Other -> + ResourceError.Other(e) } public companion object { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/Extensions.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/Extensions.kt index e96284c5df..7d32da42bb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/Extensions.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/Extensions.kt @@ -15,7 +15,7 @@ public suspend fun HttpURLConnection.sniffMediaType( bytes: (() -> ByteArray)? = null, mediaTypes: List = emptyList(), fileExtensions: List = emptyList() -): MediaType? = throw NotImplementedError() +): MediaType = throw NotImplementedError() @Suppress("UnusedReceiverParameter", "RedundantSuspendModifier", "UNUSED_PARAMETER") @Deprecated("Use your own solution instead", level = DeprecationLevel.ERROR) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt index 7770c5dbee..217a36346a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt @@ -28,55 +28,55 @@ public interface ResourceMediaTypeSnifferContent : MediaTypeSnifferContent { * See https://en.wikipedia.org/wiki/List_of_file_signatures */ public suspend fun read(range: LongRange? = null): ByteArray? +} - /** - * Content as plain text. - * - * It will extract the charset parameter from the media type hints to figure out an encoding. - * Otherwise, fallback on UTF-8. - */ - public suspend fun contentAsString(): String? = - read()?.let { - tryOrNull { - withContext(Dispatchers.Default) { String(it) } - } +/** + * Content as plain text. + * + * It will extract the charset parameter from the media type hints to figure out an encoding. + * Otherwise, fallback on UTF-8. + */ +public suspend fun ResourceMediaTypeSnifferContent.contentAsString(): String? = + read()?.let { + tryOrNull { + withContext(Dispatchers.Default) { String(it) } } + } - /** Content as an XML document. */ - public suspend fun contentAsXml(): ElementNode? = - read()?.let { - tryOrNull { - withContext(Dispatchers.Default) { - XmlParser().parse(ByteArrayInputStream(it)) - } +/** Content as an XML document. */ +public suspend fun ResourceMediaTypeSnifferContent.contentAsXml(): ElementNode? = + read()?.let { + tryOrNull { + withContext(Dispatchers.Default) { + XmlParser().parse(ByteArrayInputStream(it)) } } + } - /** - * Content parsed from JSON. - */ - public suspend fun contentAsJson(): JSONObject? = - contentAsString()?.let { - tryOrNull { - withContext(Dispatchers.Default) { - JSONObject(it) - } +/** + * Content parsed from JSON. + */ +public suspend fun ResourceMediaTypeSnifferContent.contentAsJson(): JSONObject? = + contentAsString()?.let { + tryOrNull { + withContext(Dispatchers.Default) { + JSONObject(it) } } + } - /** Readium Web Publication Manifest parsed from the content. */ - public suspend fun contentAsRwpm(): Manifest? = - Manifest.fromJSON(contentAsJson()) +/** Readium Web Publication Manifest parsed from the content. */ +public suspend fun ResourceMediaTypeSnifferContent.contentAsRwpm(): Manifest? = + Manifest.fromJSON(contentAsJson()) - /** - * Raw bytes stream of the content. - * - * A byte stream can be useful when sniffers only need to read a few bytes at the beginning of - * the file. - */ - public suspend fun contentAsStream(): InputStream = - ByteArrayInputStream(read() ?: ByteArray(0)) -} +/** + * Raw bytes stream of the content. + * + * A byte stream can be useful when sniffers only need to read a few bytes at the beginning of + * the file. + */ +public suspend fun ResourceMediaTypeSnifferContent.contentAsStream(): InputStream = + ByteArrayInputStream(read() ?: ByteArray(0)) /** * Returns whether the content is a JSON object containing all of the given root keys. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt index 4a6f4c0589..5a13c838d7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt @@ -10,13 +10,14 @@ import kotlinx.coroutines.runBlocking import org.readium.r2.shared.extensions.read import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.mediatype.MediaType public sealed class BaseBytesResource( override val source: AbsoluteUrl?, private val mediaType: MediaType, private val properties: Resource.Properties, - protected val bytes: suspend () -> Try + protected val bytes: suspend () -> Try ) : Resource { override suspend fun properties(): ResourceTry = @@ -28,7 +29,7 @@ public sealed class BaseBytesResource( override suspend fun length(): ResourceTry = read().map { it.size.toLong() } - private lateinit var _bytes: Try + private lateinit var _bytes: Try override suspend fun read(range: LongRange?): ResourceTry { if (!::_bytes.isInitialized) { @@ -92,5 +93,5 @@ public class StringResource( this(source = url, mediaType = mediaType, properties = properties, { Try.success(string) }) override fun toString(): String = - "${javaClass.simpleName}(${runBlocking { readAsString() }})" + "${javaClass.simpleName}(${runBlocking { read().getOrThrow().decodeToString() } }})" } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt index 5cc9a2d487..a92f6d4ae9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt @@ -51,7 +51,7 @@ public class EmptyContainer : Container { override suspend fun entries(): Set = emptySet() override fun get(url: Url): Container.Entry = - FailureResource(Resource.Exception.NotFound()).toEntry(url) + FailureResource(ResourceError.NotFound()).toEntry(url) override suspend fun close() {} } @@ -65,7 +65,7 @@ public class ResourceContainer(url: Url, resource: Resource) : Container { override fun get(url: Url): Container.Entry { if (url.removeFragment().removeQuery() != entry.url) { - return FailureResource(Resource.Exception.NotFound()).toEntry(url) + return FailureResource(ResourceError.NotFound()).toEntry(url) } return entry diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt index 5e1b9d0434..605e88fae7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt @@ -118,30 +118,33 @@ public class ContentResource internal constructor( return _length } - private suspend fun withStream(block: suspend (InputStream) -> T): Try = - ResourceTry.catching { + private suspend fun withStream(block: suspend (InputStream) -> T): Try { + return ResourceTry.catching { val stream = contentResolver.openInputStream(uri) - ?: throw Resource.Exception.Unavailable( - Exception("Content provider recently crashed.") + ?: return Try.failure( + ResourceError.Unavailable( + Exception("Content provider recently crashed.") + ) ) val result = block(stream) stream.close() result } + } private inline fun Try.Companion.catching(closure: () -> T): ResourceTry = try { success(closure()) } catch (e: FileNotFoundException) { - failure(Resource.Exception.NotFound(e)) + failure(ResourceError.NotFound(e)) } catch (e: SecurityException) { - failure(Resource.Exception.Forbidden(e)) + failure(ResourceError.Forbidden(e)) } catch (e: IOException) { - failure(Resource.Exception.Unavailable(e)) + failure(ResourceError.Unavailable(e)) } catch (e: Exception) { - failure(Resource.Exception.wrap(e)) + failure(ResourceError.Other(e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - failure(Resource.Exception.wrap(e)) + failure(ResourceError.OutOfMemory(e)) } override fun toString(): String = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index f14550df50..ebdebb69ee 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -57,7 +57,7 @@ internal class DirectoryContainer( ?.let { File(root, it) } return if (file == null || !root.isParentOf(file)) { - FailureResource(Resource.Exception.NotFound()).toEntry(url) + FailureResource(ResourceError.NotFound()).toEntry(url) } else { FileEntry(url, file) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt index 6f820c8ad1..a9763ce305 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt @@ -110,15 +110,8 @@ public interface ArchiveFactory { } public class ResourceReading( - cause: SharedError?, - public val resourceException: Resource.Exception - ) : Error("An error occurred while attempting to read the resource.", cause) { - - public constructor(exception: Resource.Exception) : this( - ThrowableError(exception), - exception - ) - } + override val cause: ResourceError + ) : Error("An error occurred while attempting to read the resource.", cause) } /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt index 950e7e05ad..c81c0f0568 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt @@ -14,7 +14,7 @@ import org.readium.r2.shared.util.mediatype.MediaType */ public class FallbackResource( private val originalResource: Resource, - private val fallbackResourceFactory: (Resource.Exception) -> Resource? + private val fallbackResourceFactory: (ResourceError) -> Resource? ) : Resource { override val source: AbsoluteUrl? = null @@ -63,7 +63,7 @@ public class FallbackResource( * Falls back to alternative resources when the receiver fails. */ public fun Resource.fallback( - fallbackResourceFactory: (Resource.Exception) -> Resource? + fallbackResourceFactory: (ResourceError) -> Resource? ): Resource = FallbackResource(this, fallbackResourceFactory) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt index 57cba9ef76..ee54e79413 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt @@ -6,7 +6,6 @@ package org.readium.r2.shared.util.resource -import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.nio.channels.Channels @@ -20,7 +19,6 @@ import org.readium.r2.shared.util.mediatype.MediaType internal class FileChannelResource( override val source: AbsoluteUrl?, - private val file: File?, private val channel: FileChannel ) : Resource { @@ -82,7 +80,7 @@ internal class FileChannelResource( check(channel.isOpen) Try.success(channel.size()) } catch (e: IOException) { - Try.failure(Resource.Exception.Unavailable(e)) + Try.failure(ResourceError.Unavailable(e)) } } } @@ -94,13 +92,13 @@ internal class FileChannelResource( try { success(closure()) } catch (e: FileNotFoundException) { - failure(Resource.Exception.NotFound(e)) + failure(ResourceError.NotFound(e)) } catch (e: SecurityException) { - failure(Resource.Exception.Forbidden(e)) + failure(ResourceError.Forbidden(e)) } catch (e: Exception) { - failure(Resource.Exception.wrap(e)) + failure(ResourceError.Other(e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - failure(Resource.Exception.wrap(e)) + failure(ResourceError.OutOfMemory(e)) } override fun toString(): String = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt index a0f7d4db6a..00596a4b27 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt @@ -40,8 +40,10 @@ public class FileResource private constructor( ) private val randomAccessFile by lazy { - ResourceTry.catching { - RandomAccessFile(file, "r") + try { + Try.success(RandomAccessFile(file, "r")) + } catch (e: FileNotFoundException) { + Try.failure(e) } } @@ -119,13 +121,13 @@ public class FileResource private constructor( try { success(closure()) } catch (e: FileNotFoundException) { - failure(Resource.Exception.NotFound(e)) + failure(ResourceError.NotFound(e)) } catch (e: SecurityException) { - failure(Resource.Exception.Forbidden(e)) + failure(ResourceError.Forbidden(e)) } catch (e: Exception) { - failure(Resource.Exception.wrap(e)) + failure(ResourceError.Other(e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - failure(Resource.Exception.wrap(e)) + failure(ResourceError.OutOfMemory(e)) } override fun toString(): String = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt index c564294374..cdfde76430 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt @@ -64,9 +64,9 @@ public class FileZipArchiveFactory( } catch (e: ZipException) { Try.failure(ArchiveFactory.Error.FormatNotSupported(e)) } catch (e: SecurityException) { - Try.failure(ArchiveFactory.Error.ResourceReading(Resource.Exception.Forbidden(e))) + Try.failure(ArchiveFactory.Error.ResourceReading(ResourceError.Forbidden(e))) } catch (e: Exception) { - Try.failure(ArchiveFactory.Error.ResourceReading(Resource.Exception.wrap(e))) + Try.failure(ArchiveFactory.Error.ResourceReading(ResourceError.Other(e))) } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index bd65824f02..f0e12eece2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -8,21 +8,22 @@ package org.readium.r2.shared.util.resource import android.graphics.Bitmap import android.graphics.BitmapFactory -import androidx.annotation.StringRes import java.io.ByteArrayInputStream import java.nio.charset.Charset import org.json.JSONObject -import org.readium.r2.shared.R -import org.readium.r2.shared.UserException +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.SuspendingCloseable +import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.xml.ElementNode import org.readium.r2.shared.util.xml.XmlParser -public typealias ResourceTry = Try +public typealias ResourceTry = Try /** * Acts as a proxy to an actual resource by handling read access. @@ -77,79 +78,86 @@ public interface Resource : SuspendingCloseable { * available length automatically. */ public suspend fun read(range: LongRange? = null): ResourceTry +} + +/** + * Errors occurring while accessing a resource. + */ +public sealed class ResourceError( + override val message: String, + override val cause: Error? = null +) : Error { + + /** Equivalent to a 400 HTTP error. */ + public class BadRequest(cause: Error? = null) : + ResourceError("Invalid request which can't be processed", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + /** Equivalent to a 404 HTTP error. */ + public class NotFound(cause: Error? = null) : + ResourceError("Resource not found", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } /** - * Errors occurring while accessing a resource. + * Equivalent to a 403 HTTP error. + * + * This can be returned when trying to read a resource protected with a DRM that is not + * unlocked. */ - public sealed class Exception(@StringRes userMessageId: Int, cause: Throwable? = null) : UserException( - userMessageId, - cause = cause - ) { - - /** Equivalent to a 400 HTTP error. */ - public class BadRequest(cause: Throwable? = null) : - Exception(R.string.readium_shared_resource_exception_bad_request, cause) - - /** Equivalent to a 404 HTTP error. */ - public class NotFound(cause: Throwable? = null) : - Exception(R.string.readium_shared_resource_exception_not_found, cause) - - /** - * Equivalent to a 403 HTTP error. - * - * This can be returned when trying to read a resource protected with a DRM that is not - * unlocked. - */ - public class Forbidden(cause: Throwable? = null) : - Exception(R.string.readium_shared_resource_exception_forbidden, cause) - - /** - * Equivalent to a 503 HTTP error. - * - * Used when the source can't be reached, e.g. no Internet connection, or an issue with the - * file system. Usually this is a temporary error. - */ - public class Unavailable(cause: Throwable? = null) : - Exception(R.string.readium_shared_resource_exception_unavailable, cause) - - /** - * The Internet connection appears to be offline. - */ - public object Offline : Exception(R.string.readium_shared_resource_exception_offline) - - /** - * Equivalent to a 507 HTTP error. - * - * Used when the requested range is too large to be read in memory. - */ - public class OutOfMemory(override val cause: OutOfMemoryError) : - Exception(R.string.readium_shared_resource_exception_out_of_memory) - - /** For any other error, such as HTTP 500. */ - public class Other(cause: Throwable) : Exception( - R.string.readium_shared_resource_exception_other, - cause - ) + public class Forbidden(cause: Error? = null) : + ResourceError("You are not allowed to access the resource.", cause) { + public constructor(exception: Exception) : this(ThrowableError(exception)) + } - public companion object { + /** + * Equivalent to a 503 HTTP error. + * + * Used when the source can't be reached, e.g. no Internet connection, or an issue with the + * file system. Usually this is a temporary error. + */ + public class Unavailable(cause: Error? = null) : + ResourceError("The resource is currently unavailable, please try again later.", cause) { - public fun wrap(e: Throwable): Exception = - when (e) { - is Exception -> e - is OutOfMemoryError -> OutOfMemory(e) - else -> Other(e) - } + public constructor(exception: Exception) : this(ThrowableError(exception)) } + + /** + * The Internet connection appears to be offline. + */ + public object Offline : ResourceError("The Internet connection appears to be offline.") + + /** + * Equivalent to a 507 HTTP error. + * + * Used when the requested range is too large to be read in memory. + */ + public class OutOfMemory(override val cause: ThrowableError) : + ResourceError("The resource is too large to be read on this device.", cause) { + + public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) + } + + public class InvalidContent(cause: Error?) + : ResourceError("Content seems invalid. ", cause) + + /** For any other error, such as HTTP 500. */ + public class Other(cause: Error) : ResourceError("A service error occurred", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) } + + internal companion object } /** Creates a Resource that will always return the given [error]. */ public class FailureResource( - private val error: Resource.Exception + private val error: ResourceError ) : Resource { - internal constructor(cause: Throwable) : this(Resource.Exception.wrap(cause)) - override val source: AbsoluteUrl? = null override suspend fun mediaType(): ResourceTry = Try.failure(error) override suspend fun properties(): ResourceTry = Try.failure(error) @@ -161,51 +169,98 @@ public class FailureResource( "${javaClass.simpleName}($error)" } + /** * Maps the result with the given [transform] * * If the [transform] throws an [Exception], it is wrapped in a failure with Resource.Exception.Other. */ -public inline fun ResourceTry.mapCatching(transform: (value: S) -> R): ResourceTry = - try { - map(transform) - } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(e)) - } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - Try.failure(Resource.Exception.wrap(e)) - } + +@Deprecated("Catch exceptions yourself to the most suitable ResourceError.", level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("map(transform)") +) +@Suppress("UnusedReceiverParameter") +public fun ResourceTry.mapCatching(): ResourceTry = + throw NotImplementedError() + public inline fun ResourceTry.flatMapCatching(transform: (value: S) -> ResourceTry): ResourceTry = - mapCatching(transform).flatMap { it } + flatMap { + try { + transform(it) + } catch (e: Exception) { + Try.failure(ResourceError.Other(e)) + } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. + Try.failure(ResourceError.OutOfMemory(e)) + } + } + +@InternalReadiumApi +public fun ResourceTry.decode( + block: (value: S) -> R, + errorMessage: () -> String +): ResourceTry = + when (this) { + is Try.Success -> + try { + Try.success( + block(value) + ) + } catch (e: Exception) { + Try.failure( + ResourceError.InvalidContent( + MessageError(errorMessage()) + ) + ) + } + is Try.Failure -> + Try.failure(value) + } /** * Reads the full content as a [String]. * - * If [charset] is null, then it is parsed from the `charset` parameter of link().type, - * or falls back on UTF-8. + * If [charset] is null, then it falls back on UTF-8. */ public suspend fun Resource.readAsString(charset: Charset? = null): ResourceTry = - read().mapCatching { - String(it, charset = charset ?: Charsets.UTF_8) - } + read() + .decode( + { String(it, charset = charset ?: Charsets.UTF_8) }, + { "Content doesn't seem to be a valid string." } + ) /** * Reads the full content as a JSON object. */ public suspend fun Resource.readAsJson(): ResourceTry = - readAsString(charset = Charsets.UTF_8).mapCatching { JSONObject(it) } + readAsString(charset = Charsets.UTF_8) + .decode( + { JSONObject(it) }, + { "Content doesn't seem to be valid JSON." } + ) + /** * Reads the full content as an XML document. */ public suspend fun Resource.readAsXml(): ResourceTry = - read().mapCatching { XmlParser().parse(ByteArrayInputStream(it)) } + read() + .decode( + { XmlParser().parse(ByteArrayInputStream(it)) }, + { "Content doesn't seem to be valid XML." } + ) /** * Reads the full content as a [Bitmap]. */ public suspend fun Resource.readAsBitmap(): ResourceTry = - read().mapCatching { - BitmapFactory.decodeByteArray(it, 0, it.size) - ?: throw kotlin.Exception("Could not decode resource as a bitmap") - } + read() + .flatMap { bytes -> + BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + ?.let { Try.success(it) } + ?: Try.failure( + ResourceError.InvalidContent( + MessageError("Could not decode resource as a bitmap.") + ) + ) + } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt index a7fa47e118..f7477a8bb3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt @@ -42,7 +42,7 @@ public class RoutingContainer(private val routes: List) : Container { override fun get(url: Url): Container.Entry = routes.firstOrNull { it.accepts(url) }?.container?.get(url) - ?: FailureResource(Resource.Exception.NotFound()).toEntry(url) + ?: FailureResource(ResourceError.NotFound()).toEntry(url) override suspend fun close() { routes.forEach { it.container.close() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt index dda194d101..532ee45b6d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt @@ -65,20 +65,20 @@ public data class ArchiveProperties( } } -private const val archiveKey = "archive" +private const val ARCHIVE_KEY = "archive" public val Resource.Properties.archive: ArchiveProperties? - get() = (this[archiveKey] as? Map<*, *>) + get() = (this[ARCHIVE_KEY] as? Map<*, *>) ?.let { ArchiveProperties.fromJSON(JSONObject(it)) } public var Resource.Properties.Builder.archive: ArchiveProperties? - get() = (this[archiveKey] as? Map<*, *>) + get() = (this[ARCHIVE_KEY] as? Map<*, *>) ?.let { ArchiveProperties.fromJSON(JSONObject(it)) } set(value) { if (value == null) { - remove(archiveKey) + remove(ARCHIVE_KEY) } else { - put(archiveKey, value.toJSON().toMap()) + put(ARCHIVE_KEY, value.toJSON().toMap()) } } @@ -101,13 +101,13 @@ internal class JavaZipContainer( ) override suspend fun properties(): ResourceTry = - Try.failure(Resource.Exception.NotFound()) + Try.failure(ResourceError.NotFound()) override suspend fun length(): ResourceTry = - Try.failure(Resource.Exception.NotFound()) + Try.failure(ResourceError.NotFound()) override suspend fun read(range: LongRange?): ResourceTry = - Try.failure(Resource.Exception.NotFound()) + Try.failure(ResourceError.NotFound()) override suspend fun close() { } @@ -136,10 +136,10 @@ internal class JavaZipContainer( } ) - override suspend fun length(): Try = + override suspend fun length(): Try = entry.size.takeUnless { it == -1L } ?.let { Try.success(it) } - ?: Try.failure(Resource.Exception.Other(Exception("Unsupported operation"))) + ?: Try.failure(ResourceError.Other(Exception("Unsupported operation"))) private val compressedLength: Long? = if (entry.method == ZipEntry.STORED || entry.method == -1) { @@ -148,7 +148,7 @@ internal class JavaZipContainer( entry.compressedSize.takeUnless { it == -1L } } - override suspend fun read(range: LongRange?): Try = + override suspend fun read(range: LongRange?): Try = try { withContext(Dispatchers.IO) { val bytes = @@ -160,9 +160,9 @@ internal class JavaZipContainer( Try.success(bytes) } } catch (e: IOException) { - Try.failure(Resource.Exception.Unavailable(e)) + Try.failure(ResourceError.Unavailable(e)) } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(e)) + Try.failure(ResourceError.Other(e)) } private suspend fun readFully(): ByteArray = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt index 8de612337e..8c76456a14 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt @@ -6,6 +6,7 @@ package org.readium.r2.shared.util.resource.content +import org.readium.r2.shared.util.Error as SharedError import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jsoup.Jsoup @@ -14,8 +15,8 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.ResourceTry -import org.readium.r2.shared.util.resource.mapCatching import org.readium.r2.shared.util.resource.readAsString /** @@ -37,6 +38,19 @@ public interface ResourceContentExtractor { */ public suspend fun createExtractor(resource: Resource): ResourceContentExtractor? } + + public sealed class Error( + public override val message: String + ) : SharedError { + + public class Resource( + override val cause: ResourceError? + ) : Error("An error occurred while attempting to read the resource.") + + public class Content( + override val cause: org.readium.r2.shared.util.Error? + ) : Error("Resource content doesn't match what was expected.") + } } @ExperimentalReadiumApi @@ -58,10 +72,12 @@ public class HtmlResourceContentExtractor : ResourceContentExtractor { override suspend fun extractText(resource: Resource): ResourceTry = withContext( Dispatchers.IO ) { - resource.readAsString().mapCatching { html -> - val body = Jsoup.parse(html).body().text() - // Transform HTML entities into their actual characters. - Parser.unescapeEntities(body, false) + resource + .readAsString() + .map { html -> + val body = Jsoup.parse(html).body().text() + // Transform HTML entities into their actual characters. + Parser.unescapeEntities(body, false) } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index 80eecf39be..7765244d28 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -23,6 +23,7 @@ import org.readium.r2.shared.util.resource.ArchiveProperties import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.FailureResource import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.ResourceMediaTypeSnifferContent import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.archive @@ -37,7 +38,7 @@ internal class ChannelZipContainer( private inner class FailureEntry( override val url: Url - ) : Container.Entry, Resource by FailureResource(Resource.Exception.NotFound()) + ) : Container.Entry, Resource by FailureResource(ResourceError.NotFound()) private inner class Entry( override val url: Url, @@ -68,7 +69,7 @@ internal class ChannelZipContainer( override suspend fun length(): ResourceTry = entry.size.takeUnless { it == -1L } ?.let { Try.success(it) } - ?: Try.failure(Resource.Exception.Other(UnsupportedOperationException())) + ?: Try.failure(ResourceError.Other(UnsupportedOperationException())) private val compressedLength: Long? get() = @@ -88,8 +89,10 @@ internal class ChannelZipContainer( readRange(range) } Try.success(bytes) + } catch (e: ResourceChannel.ResourceException) { + Try.failure(e.error) } catch (e: Exception) { - Try.failure(Resource.Exception.wrap(e)) + Try.failure(ResourceError.Other(e)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/HttpChannel.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/HttpChannel.kt index 6a0cde1355..91f3ac96b6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/HttpChannel.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/HttpChannel.kt @@ -18,7 +18,7 @@ import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpException +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpResponse import org.readium.r2.shared.util.http.head @@ -43,9 +43,9 @@ internal class HttpChannel( private var inputStreamStart = 0L /** Cached HEAD response to get the expected content length and other metadata. */ - private lateinit var _headResponse: Try + private lateinit var _headResponse: Try - private suspend fun headResponse(): Try { + private suspend fun headResponse(): Try { if (::_headResponse.isInitialized) { return _headResponse } @@ -60,7 +60,7 @@ internal class HttpChannel( * The stream is cached and reused for next calls, if the next [from] offset is in a forward * direction. */ - private suspend fun stream(from: Long? = null): Try { + private suspend fun stream(from: Long? = null): Try { Timber.d("getStream") val stream = inputStream if (from != null && stream != null) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ResourceChannel.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ResourceChannel.kt index f8e8943f97..4b8271b24a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ResourceChannel.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ResourceChannel.kt @@ -8,10 +8,18 @@ package org.readium.r2.shared.util.zip import java.io.IOException import java.nio.ByteBuffer -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.zip.jvm.ClosedChannelException import org.readium.r2.shared.util.zip.jvm.NonWritableChannelException import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel @@ -20,6 +28,10 @@ internal class ResourceChannel( private val resource: Resource ) : SeekableByteChannel { + class ResourceException( + val error: ResourceError, + ) : IOException(error.message) + private val coroutineScope: CoroutineScope = MainScope() @@ -50,7 +62,7 @@ internal class ResourceChannel( withContext(Dispatchers.IO) { val size = resource.length() - .getOrElse { throw IOException("Content length not available.", it) } + .getOrElse { throw ResourceException(it) } if (position >= size) { return@withContext -1 @@ -60,8 +72,7 @@ internal class ResourceChannel( val toBeRead = dst.remaining().coerceAtMost(available.toInt()) check(toBeRead > 0) val bytes = resource.read(position until position + toBeRead) - .mapFailure { IOException(it) } - .getOrThrow() + .getOrElse { throw ResourceException(it) } check(bytes.size == toBeRead) dst.put(bytes, 0, toBeRead) position += toBeRead @@ -93,7 +104,7 @@ internal class ResourceChannel( } return runBlocking { resource.length() } - .mapFailure { IOException(it) } + .mapFailure { IOException(ResourceException(it)) } .getOrThrow() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt index c39cfea1e8..069b5f89aa 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt @@ -57,8 +57,8 @@ public class StreamingZipArchiveFactory( val zipFile = ZipFile(channel, true) val channelZip = ChannelZipContainer(zipFile, resource.source, mediaTypeRetriever) Try.success(channelZip) - } catch (e: Resource.Exception) { - Try.failure(ArchiveFactory.Error.ResourceReading(e)) + } catch (e: ResourceChannel.ResourceException) { + Try.failure(ArchiveFactory.Error.ResourceReading(e.error)) } catch (e: Exception) { Try.failure(ArchiveFactory.Error.FormatNotSupported(e)) } diff --git a/readium/shared/src/main/res/values/strings.xml b/readium/shared/src/main/res/values/strings.xml index 37cf161bf8..69de7009d7 100644 --- a/readium/shared/src/main/res/values/strings.xml +++ b/readium/shared/src/main/res/values/strings.xml @@ -15,31 +15,4 @@ This publication cannot be opened because it is protected with %1$s This publication cannot be opened because it is protected with an unknown DRM - - Invalid request which can\'t be processed - Resource not found - You are not allowed to access the resource - The resource is currently unavailable, please try again later - The Internet connection appears to be offline - The resource is too large to be read on this device - An expected error occurred. - A service error occurred - - The provided request was not valid - The received response could not be decoded - Request timed out - The provided request was not valid - Authentication required - You are not authorized - Page not found - Method not allowed - A client error occurred - A server error occurred, please try again later - Your Internet connection appears to be offline - The request was cancelled - A networking error occurred - - This publication is not searchable - The search was cancelled - An error occurred while searching \ No newline at end of file diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt index e4e0ee9e69..418cfc2568 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt @@ -35,16 +35,16 @@ class TestContainer(resources: Map = emptyMap()) : Container { override val source: AbsoluteUrl? = null override suspend fun mediaType(): ResourceTry = - Try.failure(Resource.Exception.NotFound()) + Try.failure(Resource.Error.NotFound()) override suspend fun properties(): ResourceTry = - Try.failure(Resource.Exception.NotFound()) + Try.failure(Resource.Error.NotFound()) override suspend fun length(): ResourceTry = - Try.failure(Resource.Exception.NotFound()) + Try.failure(Resource.Error.NotFound()) override suspend fun read(range: LongRange?): ResourceTry = - Try.failure(Resource.Exception.NotFound()) + Try.failure(Resource.Error.NotFound()) override suspend fun close() { } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt index bc51db10b4..4d2c7c2205 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt @@ -42,7 +42,7 @@ class DirectoryContainerTest { @Test fun `Reading a missing file returns NotFound`() { val resource = sut().get(Url("unknown")!!) - assertIs(resource.readBlocking().failureOrNull()) + assertIs(resource.readBlocking().failureOrNull()) } @Test @@ -62,13 +62,13 @@ class DirectoryContainerTest { @Test fun `Reading a directory returns NotFound`() { val resource = sut().get(Url("subdirectory")!!) - assertIs(resource.readBlocking().failureOrNull()) + assertIs(resource.readBlocking().failureOrNull()) } @Test fun `Reading a file outside the allowed directory returns NotFound`() { val resource = sut().get(Url("../epub.epub")!!) - assertIs(resource.readBlocking().failureOrNull()) + assertIs(resource.readBlocking().failureOrNull()) } @Test @@ -114,13 +114,13 @@ class DirectoryContainerTest { @Test fun `Computing a directory length returns NotFound`() { val resource = sut().get(Url("subdirectory")!!) - assertIs(resource.lengthBlocking().failureOrNull()) + assertIs(resource.lengthBlocking().failureOrNull()) } @Test fun `Computing the length of a missing file returns NotFound`() { val resource = sut().get(Url("unknown")!!) - assertIs(resource.lengthBlocking().failureOrNull()) + assertIs(resource.lengthBlocking().failureOrNull()) } @Test diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index ae8f5f3d84..212cd6823f 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -9,8 +9,8 @@ package org.readium.r2.streamer import java.nio.charset.Charset import org.json.JSONObject import org.readium.r2.shared.extensions.addPrefix +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.publication.Manifest -import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try @@ -37,7 +37,7 @@ internal class ParserAssetFactory( suspend fun createParserAsset( asset: Asset - ): Try { + ): Try { return when (asset) { is Asset.Container -> createParserAssetForContainer(asset) @@ -48,7 +48,7 @@ internal class ParserAssetFactory( private fun createParserAssetForContainer( asset: Asset.Container - ): Try = + ): Try = Try.success( PublicationParser.Asset( mediaType = asset.mediaType, @@ -58,7 +58,7 @@ internal class ParserAssetFactory( private suspend fun createParserAssetForResource( asset: Asset.Resource - ): Try = + ): Try = if (asset.mediaType.isRwpm) { createParserAssetForManifest(asset) } else { @@ -67,10 +67,10 @@ internal class ParserAssetFactory( private suspend fun createParserAssetForManifest( asset: Asset.Resource - ): Try { + ): Try { val manifest = asset.resource.readAsRwpm() .mapFailure { - Publication.OpenError.InvalidAsset( + AssetError.InvalidAsset( "Failed to read the publication as a RWPM", ThrowableError(it) ) @@ -83,12 +83,12 @@ internal class ParserAssetFactory( } else { if (baseUrl !is AbsoluteUrl) { return Try.failure( - Publication.OpenError.InvalidAsset("Self link is not absolute.") + AssetError.InvalidAsset("Self link is not absolute.") ) } if (!baseUrl.isHttp) { return Try.failure( - Publication.OpenError.UnsupportedAsset( + AssetError.UnsupportedAsset( "Self link doesn't use the HTTP(S) scheme." ) ) @@ -114,7 +114,7 @@ internal class ParserAssetFactory( private fun createParserAssetForContent( asset: Asset.Resource - ): Try { + ): Try { // Historically, the reading order of a standalone file contained a single link with the // HREF "/$assetName". This was fragile if the asset named changed, or was different on // other devices. To avoid this, we now use a single link with the HREF diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 2cba6c5ee1..6604bfe770 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -13,7 +13,7 @@ import org.readium.r2.shared.publication.protection.AdeptFallbackContentProtecti import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.publication.protection.LcpFallbackContentProtection import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpClient @@ -21,7 +21,7 @@ import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.pdf.PdfDocumentFactory -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.streamer.parser.PublicationParser import org.readium.r2.streamer.parser.audio.AudioParser import org.readium.r2.streamer.parser.epub.EpubParser @@ -29,7 +29,7 @@ import org.readium.r2.streamer.parser.image.ImageParser import org.readium.r2.streamer.parser.pdf.PdfParser import org.readium.r2.streamer.parser.readium.ReadiumWebPubParser -internal typealias PublicationTry = Try +internal typealias PublicationTry = Try /** * Opens a Publication using a list of parsers. @@ -162,7 +162,7 @@ public class PublicationFactory( asset: Asset, onCreatePublication: Publication.Builder.() -> Unit, warnings: WarningLogger? - ): Try { + ): Try { val parserAsset = parserAssetFactory.createParserAsset(asset) .getOrElse { return Try.failure(it) } return openParserAsset(parserAsset, onCreatePublication, warnings) @@ -175,11 +175,11 @@ public class PublicationFactory( allowUserInteraction: Boolean, onCreatePublication: Publication.Builder.() -> Unit, warnings: WarningLogger? - ): Try { + ): Try { val protectedAsset = contentProtections[contentProtectionScheme] ?.open(asset, credentials, allowUserInteraction) ?.getOrElse { return Try.failure(it) } - ?: return Try.failure(Publication.OpenError.Forbidden()) + ?: return Try.failure(AssetError.Forbidden()) val parserAsset = PublicationParser.Asset( protectedAsset.mediaType, @@ -198,7 +198,7 @@ public class PublicationFactory( publicationAsset: PublicationParser.Asset, onCreatePublication: Publication.Builder.() -> Unit = {}, warnings: WarningLogger? = null - ): Try { + ): Try { val builder = parse(publicationAsset, warnings) .getOrElse { return Try.failure(wrapParserException(it)) } @@ -216,34 +216,32 @@ public class PublicationFactory( val result = parser.parse(publicationAsset, warnings) if ( result is Try.Success || - result is Try.Failure && result.value !is PublicationParser.Error.FormatNotSupported + result is Try.Failure && result.value !is PublicationParser.Error.UnsupportedFormat ) { return result } } - return Try.failure(PublicationParser.Error.FormatNotSupported()) + return Try.failure(PublicationParser.Error.UnsupportedFormat()) } - private fun wrapParserException(e: PublicationParser.Error): Publication.OpenError = + private fun wrapParserException(e: PublicationParser.Error): AssetError = when (e) { - is PublicationParser.Error.FormatNotSupported -> - Publication.OpenError.UnsupportedAsset("Cannot find a parser for this asset") - is PublicationParser.Error.IO -> - when (e.resourceError) { - is Resource.Exception.BadRequest, is Resource.Exception.Other -> - Publication.OpenError.Unknown(e) - is Resource.Exception.Forbidden -> - Publication.OpenError.Forbidden(e) - is Resource.Exception.NotFound -> - Publication.OpenError.InvalidAsset(e) - is Resource.Exception.OutOfMemory -> - Publication.OpenError.OutOfMemory(e) - is Resource.Exception.Unavailable, is Resource.Exception.Offline -> - Publication.OpenError.Unavailable(e) + is PublicationParser.Error.UnsupportedFormat -> + AssetError.UnsupportedAsset("Cannot find a parser for this asset") + is PublicationParser.Error.ResourceReading -> + when (e.cause) { + is ResourceError.BadRequest, is ResourceError.Other -> + AssetError.Unknown(e) + is ResourceError.Forbidden -> + AssetError.Forbidden(e) + is ResourceError.NotFound -> + AssetError.InvalidAsset(e) + is ResourceError.OutOfMemory -> + AssetError.OutOfMemory(e) + is ResourceError.Unavailable, is ResourceError.Offline -> + AssetError.Unavailable(e) } - is PublicationParser.Error.OutOfMemory -> - Publication.OpenError.OutOfMemory(e) is PublicationParser.Error.ParsingFailed -> - Publication.OpenError.InvalidAsset(e) + AssetError.InvalidAsset(e) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/container/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/container/Container.kt index d5a71f2b77..81da13752e 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/container/Container.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/container/Container.kt @@ -10,6 +10,7 @@ package org.readium.r2.streamer.container import java.io.InputStream +import java.lang.Error /** * Container of a publication diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt index b2ae212ba8..79ea19b01f 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt @@ -8,12 +8,11 @@ package org.readium.r2.streamer.parser import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.MessageError -import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError /** * Parses a Publication from an asset. @@ -48,7 +47,7 @@ public interface PublicationParser { public sealed class Error : org.readium.r2.shared.util.Error { - public class FormatNotSupported : Error() { + public class UnsupportedFormat : Error() { override val message: String = "Asset format not supported." @@ -65,23 +64,12 @@ public interface PublicationParser { "An error occurred while parsing the publication." } - public class IO( - public val resourceError: Resource.Exception + public class ResourceReading( + override val cause: ResourceError ) : Error() { override val message: String = "An IO error occurred." - - override val cause: org.readium.r2.shared.util.Error = - ThrowableError(resourceError) - } - - public class OutOfMemory( - override val cause: org.readium.r2.shared.util.Error? - ) : Error() { - - override val message: String = - "There is not enough memory on the device to parse the publication." } } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index 872595e3ee..44253061b0 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -32,7 +32,7 @@ public class AudioParser : PublicationParser { warnings: WarningLogger? ): Try { if (!asset.mediaType.matches(MediaType.ZAB) && !asset.mediaType.isAudio) { - return Try.failure(PublicationParser.Error.FormatNotSupported()) + return Try.failure(PublicationParser.Error.UnsupportedFormat()) } val readingOrder = diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 63a94d3e65..42a1822eca 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -45,14 +45,14 @@ public class EpubParser( warnings: WarningLogger? ): Try { if (asset.mediaType != MediaType.EPUB) { - return Try.failure(PublicationParser.Error.FormatNotSupported()) + return Try.failure(PublicationParser.Error.UnsupportedFormat()) } val opfPath = getRootFilePath(asset.container) .getOrElse { return Try.failure(it) } val opfResource = asset.container.get(opfPath) val opfXmlDocument = opfResource.readAsXml() - .getOrElse { return Try.failure(PublicationParser.Error.IO(it)) } + .getOrElse { return Try.failure(PublicationParser.Error.ResourceReading(it)) } val packageDocument = PackageDocument.parse(opfXmlDocument, opfPath, mediaTypeRetriever) ?: return Try.failure(PublicationParser.Error.ParsingFailed("Invalid OPF file.")) @@ -95,7 +95,7 @@ public class EpubParser( container .get(Url("META-INF/container.xml")!!) .use { it.readAsXml() } - .getOrElse { return Try.failure(PublicationParser.Error.IO(it)) } + .getOrElse { return Try.failure(PublicationParser.Error.ResourceReading(it)) } .getFirst("rootfiles", Namespaces.OPC) ?.getFirst("rootfile", Namespaces.OPC) ?.getAttr("full-path") diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index 8a2b657f42..f0ef161715 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -32,7 +32,7 @@ public class ImageParser : PublicationParser { warnings: WarningLogger? ): Try { if (!asset.mediaType.matches(MediaType.CBZ) && !asset.mediaType.isBitmap) { - return Try.failure(PublicationParser.Error.FormatNotSupported()) + return Try.failure(PublicationParser.Error.UnsupportedFormat()) } val readingOrder = diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index dc198d80e2..948b7d6a1d 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -38,7 +38,7 @@ public class PdfParser( warnings: WarningLogger? ): Try { if (asset.mediaType != MediaType.PDF) { - return Try.failure(PublicationParser.Error.FormatNotSupported()) + return Try.failure(PublicationParser.Error.UnsupportedFormat()) } val resource = asset.container.entries()?.firstOrNull() @@ -47,7 +47,7 @@ public class PdfParser( ) val document = pdfFactory.open(resource, password = null) .getOrElse { - return Try.failure(PublicationParser.Error.IO(it)) + return Try.failure(PublicationParser.Error.ResourceReading(it)) } val tableOfContents = document.outline.toLinks(resource.url) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index a3171a99be..e8b222bb58 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -39,13 +39,13 @@ public class ReadiumWebPubParser( warnings: WarningLogger? ): Try { if (!asset.mediaType.isReadiumWebPublication) { - return Try.failure(PublicationParser.Error.FormatNotSupported()) + return Try.failure(PublicationParser.Error.UnsupportedFormat()) } val manifestJson = asset.container .get(Url("manifest.json")!!) .readAsJson() - .getOrElse { return Try.failure(PublicationParser.Error.IO(it)) } + .getOrElse { return Try.failure(PublicationParser.Error.ResourceReading(it)) } val manifest = Manifest.fromJSON( manifestJson, diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt index fc26fe3f0c..e9681ea821 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt @@ -12,7 +12,7 @@ import org.readium.r2.shared.publication.services.cover import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpException +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.fetchWithDecoder import org.readium.r2.testapp.utils.tryOrLog @@ -52,7 +52,7 @@ class CoverStorage( } } - private suspend fun HttpClient.fetchBitmap(request: HttpRequest): Try = + private suspend fun HttpClient.fetchBitmap(request: HttpRequest): Try = fetchWithDecoder(request) { response -> BitmapFactory.decodeByteArray(response.body, 0, response.body.size) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt index 4ce6487b87..8fef603d01 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt @@ -8,7 +8,7 @@ package org.readium.r2.testapp.domain import androidx.annotation.StringRes import org.readium.r2.shared.UserException -import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.testapp.R @@ -34,7 +34,7 @@ sealed class ImportError( companion object { operator fun invoke( - error: Publication.OpenError + error: AssetError ): ImportError = PublicationError( org.readium.r2.testapp.domain.PublicationError( error diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index 82b7343731..27421dd315 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -8,7 +8,7 @@ package org.readium.r2.testapp.domain import androidx.annotation.StringRes import org.readium.r2.shared.UserException -import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.testapp.R @@ -45,23 +45,23 @@ sealed class PublicationError(@StringRes userMessageId: Int) : UserException(use companion object { - operator fun invoke(error: Publication.OpenError): PublicationError = + operator fun invoke(error: AssetError): PublicationError = when (error) { - is Publication.OpenError.Forbidden -> + is AssetError.Forbidden -> Forbidden(error) - is Publication.OpenError.IncorrectCredentials -> + is AssetError.IncorrectCredentials -> IncorrectCredentials(error) - is Publication.OpenError.NotFound -> + is AssetError.NotFound -> NotFound(error) - is Publication.OpenError.OutOfMemory -> + is AssetError.OutOfMemory -> OutOfMemory(error) - is Publication.OpenError.InvalidAsset -> + is AssetError.InvalidAsset -> InvalidPublication(error) - is Publication.OpenError.Unavailable -> + is AssetError.Unavailable -> Unavailable(error) - is Publication.OpenError.Unknown -> + is AssetError.Unknown -> Unexpected(error) - is Publication.OpenError.UnsupportedAsset -> + is AssetError.UnsupportedAsset -> UnsupportedAsset(error) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index 82bddd6057..4ca75256c3 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -22,7 +22,6 @@ import org.readium.r2.shared.publication.opds.images 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.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt index 25112231fa..2b502cca62 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt @@ -19,6 +19,7 @@ import org.readium.r2.navigator.epub.EpubNavigatorFactory import org.readium.r2.navigator.pdf.PdfNavigatorFactory import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.UserException +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.allAreHtml @@ -76,7 +77,7 @@ class ReaderRepository( ) operator fun invoke( - error: Publication.OpenError + error: AssetError ): OpeningError = PublicationError( org.readium.r2.testapp.domain.PublicationError( error diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt index 40a67ec211..6f2aa91871 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt @@ -249,9 +249,9 @@ class ReaderViewModel( // Navigator.Listener - override fun onResourceLoadFailed(href: Url, error: Resource.Exception) { + override fun onResourceLoadFailed(href: Url, error: Resource.Error) { val message = when (error) { - is Resource.Exception.OutOfMemory -> "The resource is too large to be rendered on this device: $href" + is ResourceError.OutOfMemory -> "The resource is too large to be rendered on this device: $href" else -> "Failed to render the resource: $href" } activityChannel.send(ActivityCommand.ToastError(UserException(message, error))) diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/File.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/File.kt index d31bedebe5..b77f60a5ee 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/File.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/File.kt @@ -20,7 +20,7 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpException +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpResponse import org.readium.r2.shared.util.http.HttpTry @@ -87,5 +87,5 @@ private suspend fun HttpClient.download( } } } catch (e: Exception) { - Try.failure(HttpException.wrap(e)) + Try.failure(HttpError.wrap(e)) } From 8244e92884df1ecace25bafdbb972c8bbd39c357 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Thu, 26 Oct 2023 16:57:18 +0200 Subject: [PATCH 03/86] WIP --- .../adapter/pdfium/document/PdfiumDocument.kt | 4 +- .../navigator/PdfiumDocumentFragment.kt | 5 +- .../pdfium/navigator/PdfiumEngineProvider.kt | 4 +- .../navigator/PsPdfKitDocumentFragment.kt | 4 +- .../navigator/PsPdfKitEngineProvider.kt | 4 +- .../readium/r2/lcp/LcpContentProtection.kt | 16 +- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 141 ++++++++++-------- .../org/readium/r2/lcp/LcpDecryptorTest.kt | 21 ++- .../org/readium/r2/lcp/license/License.kt | 1 - .../container/ContainerLicenseContainer.kt | 2 +- .../readium/r2/lcp/service/LicensesService.kt | 2 + .../org/readium/r2/navigator/Navigator.kt | 4 +- .../navigator/epub/EpubNavigatorViewModel.kt | 5 +- .../r2/navigator/epub/WebViewServer.kt | 29 +--- .../r2/navigator/media/ExoMediaPlayer.kt | 10 +- .../readium/r2/navigator/media/MediaPlayer.kt | 4 +- .../r2/navigator/media/MediaService.kt | 9 +- .../media/audio/MetadataRetriever.kt | 5 +- .../navigator/media/tts/TtsNavigator.kt | 4 +- .../readium/navigator/media/tts/TtsPlayer.kt | 33 +++- .../media/tts/session/TtsSessionAdapter.kt | 50 +++---- .../java/org/readium/r2/opds/OPDS1Parser.kt | 5 +- .../java/org/readium/r2/opds/OPDS2Parser.kt | 3 +- .../AdeptFallbackContentProtection.kt | 2 +- .../protection/ContentProtection.kt | 2 +- .../LcpFallbackContentProtection.kt | 2 +- .../services/ContentProtectionService.kt | 21 +-- .../services/search/SearchService.kt | 27 ++-- .../java/org/readium/r2/shared/util/Error.kt | 20 ++- .../readium/r2/shared/util/NetworkError.kt | 40 +++++ .../r2/shared/util/asset/AssetError.kt | 21 ++- .../r2/shared/util/asset/AssetRetriever.kt | 42 ++---- .../android/AndroidDownloadManager.kt | 4 +- .../foreground/ForegroundDownloadManager.kt | 2 +- .../r2/shared/util/http/HttpResource.kt | 13 +- .../shared/util/resource/ContentResource.kt | 4 +- .../util/resource/FileChannelResource.kt | 7 +- .../r2/shared/util/resource/Resource.kt | 72 ++++----- .../r2/shared/util/resource/ZipContainer.kt | 2 +- .../content/ResourceContentExtractor.kt | 4 +- .../r2/shared/util/zip/ChannelZipContainer.kt | 2 +- .../r2/shared/util/zip/ResourceChannel.kt | 4 +- .../readium/r2/streamer/ParserAssetFactory.kt | 46 +++--- .../readium/r2/streamer/PublicationFactory.kt | 35 ++--- .../r2/streamer/parser/PublicationParser.kt | 61 ++++++-- .../r2/streamer/parser/audio/AudioParser.kt | 2 +- .../r2/streamer/parser/epub/EpubParser.kt | 9 +- .../parser/epub/NavigationDocumentParser.kt | 2 +- .../streamer/parser/epub/PackageDocument.kt | 2 +- .../streamer/parser/epub/ResourceAdapter.kt | 4 +- .../r2/streamer/parser/image/ImageParser.kt | 2 +- .../r2/streamer/parser/pdf/PdfParser.kt | 7 +- .../parser/readium/LcpdfPositionsService.kt | 1 + .../parser/readium/ReadiumWebPubParser.kt | 7 +- .../catalogs/CatalogFeedListViewModel.kt | 6 +- .../readium/r2/testapp/domain/Bookshelf.kt | 3 +- .../readium/r2/testapp/domain/ImportError.kt | 4 +- .../r2/testapp/domain/PublicationError.kt | 20 ++- .../r2/testapp/domain/PublicationRetriever.kt | 12 +- .../r2/testapp/reader/ReaderRepository.kt | 2 +- .../r2/testapp/reader/ReaderViewModel.kt | 4 +- .../r2/testapp/reader/tts/TtsViewModel.kt | 4 +- .../r2/testapp/utils/extensions/File.kt | 57 ------- .../utils/extensions/readium/ErrorExt.kt | 2 +- test-app/src/main/res/values/strings.xml | 14 +- 65 files changed, 501 insertions(+), 461 deletions(-) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/NetworkError.kt diff --git a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt index 99b7606446..bc8ca0c25c 100644 --- a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt +++ b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt @@ -6,10 +6,10 @@ package org.readium.adapter.pdfium.document -import com.shockwave.pdfium.PdfDocument as _PdfiumDocument import android.content.Context import android.graphics.Bitmap import android.os.ParcelFileDescriptor +import com.shockwave.pdfium.PdfDocument as _PdfiumDocument import com.shockwave.pdfium.PdfiumCore import java.io.File import kotlin.reflect.KClass @@ -108,7 +108,7 @@ public class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory() { internal interface Listener { - fun onResourceLoadFailed(href: Url, error: Resource.Error) + fun onResourceLoadFailed(href: Url, error: ResourceError) fun onConfigurePdfView(configurator: PDFView.Configurator) fun onTap(point: PointF): Boolean } diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt index cabd2a9304..9845a33ba4 100644 --- a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt @@ -19,7 +19,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Metadata import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError /** * Main component to use the PDF navigator with the PDFium adapter. @@ -49,7 +49,7 @@ public class PdfiumEngineProvider( initialPageIndex = input.pageIndex, initialSettings = input.settings, listener = object : PdfiumDocumentFragment.Listener { - override fun onResourceLoadFailed(href: Url, error: Resource.Error) { + override fun onResourceLoadFailed(href: Url, error: ResourceError) { input.navigatorListener?.onResourceLoadFailed(href, error) } diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt index 3cf9e2f166..2724475da1 100644 --- a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt @@ -55,7 +55,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.isProtected import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.pdf.cachedIn -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.ResourceTry import timber.log.Timber @@ -69,7 +69,7 @@ public class PsPdfKitDocumentFragment internal constructor( ) : PdfDocumentFragment() { internal interface Listener { - fun onResourceLoadFailed(href: Url, error: Resource.Error) + fun onResourceLoadFailed(href: Url, error: ResourceError) fun onConfigurePdfView(builder: PdfConfiguration.Builder): PdfConfiguration.Builder fun onTap(point: PointF): Boolean } diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt index 9ab9e849c1..bb25d3f806 100644 --- a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt @@ -20,7 +20,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Metadata import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError /** * Main component to use the PDF navigator with PSPDFKit. @@ -50,7 +50,7 @@ public class PsPdfKitEngineProvider( initialPageIndex = input.pageIndex, initialSettings = input.settings, listener = object : PsPdfKitDocumentFragment.Listener { - override fun onResourceLoadFailed(href: Url, error: Resource.Error) { + override fun onResourceLoadFailed(href: Url, error: ResourceError) { input.navigatorListener?.onResourceLoadFailed(href, error) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index 86da2dc16e..cd5ff8ef09 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -158,12 +158,16 @@ internal class LcpContentProtection( AssetError.Forbidden(this) is ResourceError.NotFound -> AssetError.NotFound(this) - ResourceError.Offline, is ResourceError.Unavailable -> - AssetError.Unavailable(this) - is ResourceError.Other, is ResourceError.BadRequest -> + is ResourceError.Other -> AssetError.Unknown(this) is ResourceError.OutOfMemory -> AssetError.OutOfMemory(this) + is ResourceError.Filesystem -> + AssetError.Filesystem(cause) + is ResourceError.InvalidContent -> + AssetError.InvalidAsset(this) + is ResourceError.Network -> + AssetError.Network(cause) } private fun AssetRetriever.Error.wrap(): AssetError = @@ -180,9 +184,11 @@ internal class LcpContentProtection( AssetError.OutOfMemory(this) is AssetRetriever.Error.SchemeNotSupported -> AssetError.UnsupportedAsset(this) - is AssetRetriever.Error.Unavailable -> - AssetError.Unavailable(this) is AssetRetriever.Error.Unknown -> AssetError.Unknown(this) + is AssetRetriever.Error.Filesystem -> + AssetError.Filesystem(cause) + is AssetRetriever.Error.Network -> + AssetError.Network(cause) } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index cae8d292c2..868b2aa5bd 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -9,12 +9,13 @@ package org.readium.r2.lcp -import java.io.IOException import org.readium.r2.shared.extensions.coerceFirstNonNegative import org.readium.r2.shared.extensions.inflate import org.readium.r2.shared.extensions.requireLengthFitInt import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.MessageError +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.getOrElse @@ -27,7 +28,6 @@ import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.TransformingResource import org.readium.r2.shared.util.resource.flatMap import org.readium.r2.shared.util.resource.flatMapCatching -import org.readium.r2.shared.util.resource.mapCatching /** * Decrypts a resource protected with LCP. @@ -127,30 +127,35 @@ internal class LcpDecryptor( return _length } - private suspend fun lengthFromPadding(): ResourceTry = - resource.length().flatMapCatching { length -> - if (length < 2 * AES_BLOCK_SIZE) { - throw Exception("Invalid CBC-encrypted stream") - } + private suspend fun lengthFromPadding(): ResourceTry { + val length = resource.length() + .getOrElse { return Try.failure(it) } - val readOffset = length - (2 * AES_BLOCK_SIZE) - resource.read(readOffset..length) - .mapCatching { bytes -> - val decryptedBytes = license.decrypt(bytes) - .getOrElse { - throw Exception( - "Can't decrypt trailing size of CBC-encrypted stream", - it - ) - } - check(decryptedBytes.size == AES_BLOCK_SIZE) - - return@mapCatching length - - AES_BLOCK_SIZE - // Minus IV - decryptedBytes.last().toInt() // Minus padding size - } + if (length < 2 * AES_BLOCK_SIZE) { + return Try.failure(ResourceError.InvalidContent("Invalid CBC-encrypted stream.")) } + val readOffset = length - (2 * AES_BLOCK_SIZE) + val bytes = resource.read(readOffset..length) + .getOrElse { return Try.failure(it) } + + val decryptedBytes = license.decrypt(bytes) + .getOrElse { + return Try.failure( + ResourceError.InvalidContent( + "Can't decrypt trailing size of CBC-encrypted stream" + ) + ) + } + check(decryptedBytes.size == AES_BLOCK_SIZE) + + val adjustedLength = length - + AES_BLOCK_SIZE - // Minus IV + decryptedBytes.last().toInt() // Minus padding size + + return Try.success(adjustedLength) + } + override suspend fun read(range: LongRange?): ResourceTry { if (range == null) { return license.decryptFully(resource.read(), isDeflated = false) @@ -165,49 +170,55 @@ internal class LcpDecryptor( return Try.success(ByteArray(0)) } - return resource.length().flatMapCatching { encryptedLength -> - // encrypted data is shifted by AES_BLOCK_SIZE because of IV and - // the previous block must be provided to perform XOR on intermediate blocks - val encryptedStart = range.first.floorMultipleOf(AES_BLOCK_SIZE.toLong()) - val encryptedEndExclusive = (range.last + 1).ceilMultipleOf(AES_BLOCK_SIZE.toLong()) + AES_BLOCK_SIZE - - getEncryptedData(encryptedStart until encryptedEndExclusive).mapCatching { encryptedData -> - if (encryptedData.size >= _cache.data.size) { - // cache the three last encrypted blocks that have been read for future use - val cacheStart = encryptedData.size - _cache.data.size - _cache.startIndex = (encryptedEndExclusive - _cache.data.size).toInt() - encryptedData.copyInto(_cache.data, 0, cacheStart) - } - - val bytes = license.decrypt(encryptedData) - .getOrElse { - throw IOException( + val encryptedLength = resource.length() + .getOrElse { return Try.failure(it) } + + // encrypted data is shifted by AES_BLOCK_SIZE because of IV and + // the previous block must be provided to perform XOR on intermediate blocks + val encryptedStart = range.first.floorMultipleOf(AES_BLOCK_SIZE.toLong()) + val encryptedEndExclusive = (range.last + 1).ceilMultipleOf(AES_BLOCK_SIZE.toLong()) + AES_BLOCK_SIZE + + val encryptedData = getEncryptedData(encryptedStart until encryptedEndExclusive) + .getOrElse { return Try.failure(it) } + + if (encryptedData.size >= _cache.data.size) { + // cache the three last encrypted blocks that have been read for future use + val cacheStart = encryptedData.size - _cache.data.size + _cache.startIndex = (encryptedEndExclusive - _cache.data.size).toInt() + encryptedData.copyInto(_cache.data, 0, cacheStart) + } + + val bytes = license.decrypt(encryptedData) + .getOrElse { + return Try.failure( + ResourceError.InvalidContent( + MessageError( "Can't decrypt the content for resource with key: ${resource.source}", - it + ThrowableError(it) ) - } + ) + ) + } - // exclude the bytes added to match a multiple of AES_BLOCK_SIZE - val sliceStart = (range.first - encryptedStart).toInt() + // exclude the bytes added to match a multiple of AES_BLOCK_SIZE + val sliceStart = (range.first - encryptedStart).toInt() - // was the last block read to provide the desired range - val lastBlockRead = encryptedLength - encryptedEndExclusive <= AES_BLOCK_SIZE + // was the last block read to provide the desired range + val lastBlockRead = encryptedLength - encryptedEndExclusive <= AES_BLOCK_SIZE - val rangeLength = - if (lastBlockRead) { - // use decrypted length to ensure range.last doesn't exceed decrypted length - 1 - range.last.coerceAtMost(length().getOrThrow() - 1) - range.first + 1 - } else { - // the last block won't be read, so there's no need to compute length - range.last - range.first + 1 - } + val rangeLength = + if (lastBlockRead) { + // use decrypted length to ensure range.last doesn't exceed decrypted length - 1 + range.last.coerceAtMost(length().getOrThrow() - 1) - range.first + 1 + } else { + // the last block won't be read, so there's no need to compute length + range.last - range.first + 1 + } - // keep only enough bytes to fit the length corrected request in order to never include padding - val sliceEnd = sliceStart + rangeLength.toInt() + // keep only enough bytes to fit the length corrected request in order to never include padding + val sliceEnd = sliceStart + rangeLength.toInt() - bytes.sliceArray(sliceStart until sliceEnd) - } - } + return Try.success(bytes.sliceArray(sliceStart until sliceEnd)) } private suspend fun getEncryptedData(range: LongRange): ResourceTry { @@ -235,10 +246,16 @@ internal class LcpDecryptor( } private suspend fun LcpLicense.decryptFully(data: ResourceTry, isDeflated: Boolean): ResourceTry = - data.mapCatching { encryptedData -> + data.flatMapCatching { encryptedData -> // Decrypts the resource. var bytes = decrypt(encryptedData) - .getOrElse { throw Exception("Failed to decrypt the resource", it) } + .getOrElse { + return Try.failure( + ResourceError.InvalidContent( + MessageError("Failed to decrypt the resource", ThrowableError(it)) + ) + ) + } if (bytes.isEmpty()) { throw IllegalStateException("Lcp.nativeDecrypt returned an empty ByteArray") @@ -253,7 +270,7 @@ private suspend fun LcpLicense.decryptFully(data: ResourceTry, isDefl bytes = bytes.inflate(nowrap = true) } - bytes + Try.success(bytes) } private val Encryption.isDeflated: Boolean get() = diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt index 28da6ca47e..db8699b8cb 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt @@ -7,15 +7,18 @@ * LICENSE file present in the project repository where this source code is maintained. */ +@file:Suppress("unused") + package org.readium.r2.lcp import kotlin.math.ceil import org.readium.r2.shared.extensions.coerceIn import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.mapCatching import org.readium.r2.shared.util.use import timber.log.Timber @@ -51,7 +54,10 @@ internal suspend fun checkLengthComputationIsCorrect(publication: Publication) { publication.get(link).use { resource -> resource.length() .onFailure { - throw IllegalStateException("failed to compute length of ${link.href}", it) + throw IllegalStateException( + "failed to compute length of ${link.href}", + ErrorException(it) + ) }.onSuccess { check(it == trueLength) { "computed length of ${link.href} seems to be wrong" } } @@ -122,7 +128,11 @@ internal suspend fun Resource.readByChunks( groundTruth: ByteArray, shuffle: Boolean = true ) = - length().mapCatching { length -> + try { + val length = length() + .mapFailure { ErrorException(it) } + .getOrThrow() + val blockNb = ceil(length / chunkSize.toDouble()).toInt() val blocks = (0 until blockNb) .map { Pair(it, it * chunkSize until kotlin.math.min(length, (it + 1) * chunkSize)) } @@ -140,7 +150,7 @@ internal suspend fun Resource.readByChunks( val decryptedBytes = read(it.second).getOrElse { error -> throw IllegalStateException( "unable to decrypt chunk ${it.second} from $source", - error + ErrorException(error) ) } check(decryptedBytes.isNotEmpty()) { "empty decrypted bytearray" } @@ -153,4 +163,7 @@ internal suspend fun Resource.readByChunks( } Pair(it.first, decryptedBytes) } + Try.success(Unit) + } catch (e: Exception) { + Try.failure(e) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt index eed9ab356a..a33cf1918b 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt @@ -9,7 +9,6 @@ package org.readium.r2.lcp.license -import java.lang.Error import java.net.HttpURLConnection import java.util.* import kotlinx.coroutines.CancellationException diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt index d03ea69979..7faaf238d8 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt @@ -11,7 +11,7 @@ import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError /** * Access to a License Document stored in a read-only container. diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index 9915338c91..7fd5d96ea9 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -36,6 +36,8 @@ import org.readium.r2.shared.extensions.tryOr import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt index aeb125d4af..3cb0fb9b5b 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt @@ -11,7 +11,7 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError /** * Base interface for a navigator rendering a publication. @@ -59,7 +59,7 @@ public interface Navigator { /** * Called when a publication resource failed to be loaded. */ - public fun onResourceLoadFailed(href: Url, error: Resource.Error) {} + public fun onResourceLoadFailed(href: Url, error: ResourceError) {} /** * Called when the navigator jumps to an explicit location, which might break the linear diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt index cf72220563..a8ad16e7b9 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt @@ -357,7 +357,10 @@ internal class EpubNavigatorViewModel( application, publication, servedAssets = config.servedAssets, - disableSelectionWhenProtected = config.disableSelectionWhenProtected + disableSelectionWhenProtected = config.disableSelectionWhenProtected, + onResourceLoadFailed = { url, error -> + listener?.onResourceLoadFailed(url, error) + } ) ) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt index 27e6f3b329..3530a02b8f 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt @@ -7,28 +7,22 @@ package org.readium.r2.navigator.epub import android.app.Application -import android.content.res.AssetManager import android.os.PatternMatcher import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import androidx.webkit.WebViewAssetLoader -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import org.readium.r2.navigator.epub.css.ReadiumCss import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication 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.http.HttpHeaders import org.readium.r2.shared.util.http.HttpRange -import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.ResourceInputStream -import org.readium.r2.shared.util.resource.StringResource -import org.readium.r2.shared.util.resource.fallback /** * Serves the publication resources and application assets in the EPUB navigator web views. @@ -38,7 +32,8 @@ internal class WebViewServer( private val application: Application, private val publication: Publication, servedAssets: List, - private val disableSelectionWhenProtected: Boolean + private val disableSelectionWhenProtected: Boolean, + private val onResourceLoadFailed: (Url, ResourceError) -> Unit ) { companion object { val publicationBaseHref = AbsoluteUrl("https://readium/publication/")!! @@ -48,8 +43,6 @@ internal class WebViewServer( Url.fromDecodedPath(path)?.let { assetsBaseHref.resolve(it) } } - private val assetManager: AssetManager = application.assets - /** * Serves the requests of the navigator web views. * @@ -95,7 +88,8 @@ internal class WebViewServer( ) var resource = publication.get(linkWithoutAnchor) - .fallback { errorResource(link, error = it) } + // FIXME: report loading errors through Navigator.Listener.onResourceLoadingFailed + // .fallback { errorResource(link, error = it) } if (link.mediaType?.isHtml == true) { resource = resource.injectHtml( publication, @@ -136,19 +130,6 @@ internal class WebViewServer( } } - private fun errorResource(link: Link, error: Resource.Error): Resource = - StringResource(mediaType = MediaType.XHTML) { - withContext(Dispatchers.IO) { - Try.success( - assetManager - .open("readium/error.xhtml").bufferedReader() - .use { it.readText() } - .replace("\${error}", error.getUserMessage(application)) - .replace("\${href}", link.href.toString()) - ) - } - } - private fun isServedAsset(path: String): Boolean = servedAssetPatterns.any { it.match(path) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt index 787b40c192..2653d3bd8d 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt @@ -54,8 +54,10 @@ import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationId import org.readium.r2.shared.publication.indexOfFirstWithHref +import org.readium.r2.shared.util.NetworkError +import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.toUri import timber.log.Timber @@ -200,9 +202,11 @@ public class ExoMediaPlayer( } override fun onPlayerError(error: PlaybackException) { - var resourceError: Resource.Error? = error.asInstance() + var resourceError: ResourceError? = error.asInstance() if (resourceError == null && (error.cause as? HttpDataSource.HttpDataSourceException)?.cause is UnknownHostException) { - resourceError = Resource.Error.Offline + resourceError = ResourceError.Network( + NetworkError.Offline(ThrowableError(error.cause!!)) + ) } if (resourceError != null) { diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt index 0411ef17ba..f1f2feb9ef 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationId -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError /** * Media player compatible with Android's MediaSession and handling the playback for @@ -59,7 +59,7 @@ public interface MediaPlayer { * Called when a resource failed to be loaded, for example because the Internet connection * is offline and the resource is streamed. */ - public fun onResourceLoadFailed(link: Link, error: Resource.Error) + public fun onResourceLoadFailed(link: Link, error: ResourceError) /** * Creates the [NotificationMetadata] for the given resource [link]. diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt index dd6a7c8ab1..813cf4cace 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt @@ -17,7 +17,6 @@ import android.os.Process import android.os.ResultReceiver import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.session.MediaSessionCompat -import android.widget.Toast import androidx.core.app.ServiceCompat import androidx.core.os.BundleCompat import androidx.media.MediaBrowserServiceCompat @@ -35,7 +34,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationId import org.readium.r2.shared.publication.services.cover import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import timber.log.Timber /** @@ -106,9 +105,7 @@ public open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by * * You should present the exception to the user. */ - public open fun onResourceLoadFailed(link: Link, error: Resource.Error) { - Toast.makeText(this, error.getUserMessage(this), Toast.LENGTH_LONG).show() - } + public open fun onResourceLoadFailed(link: Link, error: ResourceError) {} /** * Override to control which app can access the MediaSession through the MediaBrowserService. @@ -214,7 +211,7 @@ public open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by this@MediaService.onPlayerStopped() } - override fun onResourceLoadFailed(link: Link, error: Resource.Error) { + override fun onResourceLoadFailed(link: Link, error: ResourceError) { this@MediaService.onResourceLoadFailed(link, error) } } diff --git a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/MetadataRetriever.kt b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/MetadataRetriever.kt index bef288ee84..7c49ccb475 100644 --- a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/MetadataRetriever.kt +++ b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/MetadataRetriever.kt @@ -16,6 +16,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.runBlocking import org.readium.r2.shared.extensions.tryOrLog +import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.resource.Resource @@ -53,7 +54,7 @@ internal class MetadataRetriever( val data = runBlocking { resource.read(position until position + size) - .mapFailure { IOException("Resource error", it) } + .mapFailure { IOException("Resource error", ErrorException(it)) } .getOrThrow() } @@ -68,7 +69,7 @@ internal class MetadataRetriever( override fun getSize(): Long { return runBlocking { resource.length() - .mapFailure { IOException("Resource error", it) } + .mapFailure { IOException("Resource error", ErrorException(it)) } .getOrThrow() } } diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt index c66a4f6ff2..a2b437ab2e 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt @@ -176,7 +176,7 @@ public class TtsNavigator, public data class EngineError (val error: E) : Error() - public data class ContentError(val exception: Exception) : Error() + public data class ContentError(val error: org.readium.r2.shared.util.Error) : Error() } } @@ -274,7 +274,7 @@ public class TtsNavigator, private fun TtsPlayer.State.Error.toError(): State.Error = when (this) { - is TtsPlayer.State.Error.ContentError -> State.Error.ContentError(exception) + is TtsPlayer.State.Error.ContentError -> State.Error.ContentError(error) is TtsPlayer.State.Error.EngineError<*> -> State.Error.EngineError(error) } diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsPlayer.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsPlayer.kt index e5382fb14e..2f41beaab9 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsPlayer.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsPlayer.kt @@ -28,6 +28,9 @@ import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Locator +import org.readium.r2.shared.util.Error as SharedError +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.ThrowableError import timber.log.Timber /** @@ -104,12 +107,12 @@ internal class TtsPlayer, /** * The player is ready to play. */ - object Ready : State + data object Ready : State /** * The end of the media has been reached. */ - object Ended : State + data object Ended : State /** * The player cannot play because an error occurred. @@ -118,7 +121,7 @@ internal class TtsPlayer, data class EngineError (val error: E) : Error() - data class ContentError(val exception: Exception) : Error() + data class ContentError(val error: org.readium.r2.shared.util.Error) : Error() } } @@ -364,9 +367,11 @@ internal class TtsPlayer, playIfReadyAndNotPaused() } + @Suppress("unused") fun hasNextResource(): Boolean = utteranceMutable.value.position.resourceIndex + 1 < contentIterator.resourceCount + @Suppress("unused") fun nextResource() { coroutineScope.launch { nextResourceAsync() @@ -386,9 +391,11 @@ internal class TtsPlayer, playIfReadyAndNotPaused() } + @Suppress("MemberVisibilityCanBePrivate") fun hasPreviousResource(): Boolean = utteranceMutable.value.position.resourceIndex > 0 + @Suppress("unused") fun previousResource() { coroutineScope.launch { previousResourceAsync() @@ -438,7 +445,7 @@ internal class TtsPlayer, previousUtterance } catch (e: Exception) { - onContentError(e) + onContentException(e) return } @@ -461,7 +468,7 @@ internal class TtsPlayer, val nextUtterance = try { contentIterator.next() } catch (e: Exception) { - onContentError(e) + onContentException(e) return } @@ -480,7 +487,7 @@ internal class TtsPlayer, val startContext = try { contentIterator.startContext() } catch (e: Exception) { - onContentError(e) + onContentException(e) return } utteranceWindow = checkNotNull(startContext) @@ -519,9 +526,19 @@ internal class TtsPlayer, playbackJob?.cancel() } - private fun onContentError(exception: Exception) { + private fun onContentException(exception: Exception) { + val error = + if (exception is ErrorException) { + exception.error + } else { + ThrowableError(exception) + } + onContentError(error) + } + + private fun onContentError(error: SharedError) { playbackMutable.value = playbackMutable.value.copy( - state = State.Error.ContentError(exception) + state = State.Error.ContentError(error) ) playbackJob?.cancel() } diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt index 10673c1447..42e8a90908 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt @@ -34,6 +34,7 @@ import androidx.media3.common.util.Clock import androidx.media3.common.util.ListenerSet import androidx.media3.common.util.Size import androidx.media3.common.util.Util +import java.lang.Error import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.StateFlow @@ -42,8 +43,8 @@ import kotlinx.coroutines.flow.onEach import org.readium.navigator.media.tts.TtsEngine import org.readium.navigator.media.tts.TtsPlayer import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.util.resource.Resource -import java.lang.Error +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.resource.ResourceError /** * Adapts the [TtsPlayer] to media3 [Player] interface. @@ -918,36 +919,25 @@ internal class TtsSessionAdapter( @Suppress("Unchecked_cast") private fun TtsPlayer.State.Error.toPlaybackException(): PlaybackException = when (this) { - is TtsPlayer.State.Error.EngineError<*> -> mapEngineError(error as E) - is TtsPlayer.State.Error.ContentError -> when (exception) { - is ResourceError.BadRequest -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_IO_BAD_HTTP_STATUS) - is ResourceError.NotFound -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_IO_BAD_HTTP_STATUS) - is ResourceError.Forbidden -> - PlaybackException( - exception.message, - exception.cause, + is TtsPlayer.State.Error.EngineError<*> -> { + mapEngineError(error as E) + } + is TtsPlayer.State.Error.ContentError -> { + val errorCode = when (error) { + is ResourceError.NotFound -> + ERROR_CODE_IO_BAD_HTTP_STATUS + is ResourceError.Forbidden -> ERROR_CODE_DRM_DISALLOWED_OPERATION - ) - is ResourceError.Unavailable -> - PlaybackException( - exception.message, - exception.cause, + is ResourceError.Network -> ERROR_CODE_IO_NETWORK_CONNECTION_FAILED - ) - is ResourceError.Offline -> - PlaybackException( - exception.message, - exception.cause, - ERROR_CODE_IO_NETWORK_CONNECTION_FAILED - ) - is ResourceError.OutOfMemory -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_UNSPECIFIED) - is ResourceError.Other -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_UNSPECIFIED) - else -> - PlaybackException(exception.message, exception.cause, ERROR_CODE_UNSPECIFIED) + else -> + ERROR_CODE_UNSPECIFIED + } + PlaybackException( + error.message, + error.cause?.let { ErrorException(it) }, + errorCode + ) } } } diff --git a/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt b/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt index 4ae8afa339..294c9d45d6 100644 --- a/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt +++ b/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt @@ -15,6 +15,7 @@ import org.readium.r2.shared.extensions.toMap import org.readium.r2.shared.opds.* import org.readium.r2.shared.publication.* import org.readium.r2.shared.toJSON +import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.http.DefaultHttpClient @@ -60,7 +61,7 @@ public class OPDS1Parser { return client.fetchWithDecoder(request) { val url = Url(request.url) ?: throw Exception("Invalid URL") this.parse(it.body, url) - } + }.mapFailure { ErrorException(it) } } public fun parse(xmlData: ByteArray, url: Url): ParseData { @@ -243,7 +244,7 @@ public class OPDS1Parser { template } null - } + }.mapFailure { ErrorException(it) } } private fun parseEntry(entry: ElementNode, baseUrl: Url): Publication? { diff --git a/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt b/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt index 4ced89ca86..8553aa6ed9 100644 --- a/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt +++ b/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt @@ -24,6 +24,7 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.normalizeHrefsToBase +import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.http.DefaultHttpClient @@ -57,7 +58,7 @@ public class OPDS2Parser { return client.fetchWithDecoder(request) { val url = Url(request.url) ?: throw Exception("Invalid URL") this.parse(it.body, url) - } + }.mapFailure { ErrorException(it) } } public fun parse(jsonData: ByteArray, url: Url): ParseData { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt index 2b20a07390..ae4395e2b7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt @@ -7,12 +7,12 @@ package org.readium.r2.shared.publication.protection import org.readium.r2.shared.InternalReadiumApi -import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.publication.protection.ContentProtection.Scheme import org.readium.r2.shared.publication.services.contentProtectionServiceFactory import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.readAsXml diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index 2d17e67ce2..d80da52cc4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -12,11 +12,11 @@ package org.readium.r2.shared.publication.protection import androidx.annotation.StringRes import org.readium.r2.shared.R import org.readium.r2.shared.UserException -import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.publication.LocalizedString import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.ContentProtectionService import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Container diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index 435ff90ecf..0f94265ac5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -8,7 +8,6 @@ package org.readium.r2.shared.publication.protection import org.json.JSONObject import org.readium.r2.shared.InternalReadiumApi -import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.encryption.encryption import org.readium.r2.shared.publication.protection.ContentProtection.Scheme @@ -16,6 +15,7 @@ import org.readium.r2.shared.publication.services.contentProtectionServiceFactor import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.Container diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt index 11de34effb..624ad4fe3a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt @@ -20,6 +20,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationServicesHolder import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.publication.protection.ContentProtection +import org.readium.r2.shared.util.NetworkError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType @@ -301,14 +302,14 @@ private sealed class RouteHandler { val query = url.query val text = query.firstNamedOrNull("text") ?: return FailureResource( - ResourceError.BadRequest( - IllegalArgumentException("'text' parameter is required") + ResourceError.Network( + NetworkError.BadRequest("'text' parameter is required") ) ) val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() ?: return FailureResource( - ResourceError.BadRequest( - IllegalArgumentException("if present, 'peek' must be true or false") + ResourceError.Network( + NetworkError.BadRequest("If present, 'peek' must be true or false") ) ) @@ -341,21 +342,21 @@ private sealed class RouteHandler { val query = url.query val pageCountString = query.firstNamedOrNull("pageCount") ?: return FailureResource( - ResourceError.BadRequest( - IllegalArgumentException("'pageCount' parameter is required") + ResourceError.Network( + NetworkError.BadRequest("'pageCount' parameter is required") ) ) val pageCount = pageCountString.toIntOrNull()?.takeIf { it >= 0 } ?: return FailureResource( - ResourceError.BadRequest( - IllegalArgumentException("'pageCount' must be a positive integer") + ResourceError.Network( + NetworkError.BadRequest("'pageCount' must be a positive integer") ) ) val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() ?: return FailureResource( - ResourceError.BadRequest( - IllegalArgumentException("if present, 'peek' must be true or false") + ResourceError.Network( + NetworkError.BadRequest("if present, 'peek' must be true or false") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt index c67d4ce6f2..d97f5e0255 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt @@ -32,38 +32,41 @@ public sealed class SearchError( /** * The publication is not searchable. */ - public object PublicationNotSearchable - : SearchError("This publication is not searchable.") + public object PublicationNotSearchable : + SearchError("This publication is not searchable.") /** * The provided search query cannot be handled by the service. */ - public class BadQuery(cause: Error) - : SearchError("The provided search query cannot be handled by the service.", cause) + public class BadQuery(cause: Error) : + SearchError("The provided search query cannot be handled by the service.", cause) /** * An error occurred while accessing one of the publication's resources. */ - public class ResourceError(cause: Error) - : SearchError("An error occurred while accessing one of the publication's resources.", cause) + public class ResourceError(cause: Error) : + SearchError( + "An error occurred while accessing one of the publication's resources.", + cause + ) /** * An error occurred while performing an HTTP request. */ - public class NetworkError(cause: HttpError) - : SearchError("An error occurred while performing an HTTP request.", cause) + public class NetworkError(cause: HttpError) : + SearchError("An error occurred while performing an HTTP request.", cause) /** * The search was cancelled by the caller. * * For example, when a coroutine or a network request is cancelled. */ - public object Cancelled - : SearchError("The search was cancelled.") + public object Cancelled : + SearchError("The search was cancelled.") /** For any other custom service error. */ - public class Other(cause: Error) - : SearchError("An error occurred while searching.", cause) + public class Other(cause: Error) : + SearchError("An error occurred while searching.", cause) } /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt index 59d4775bd6..de988461fc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt @@ -36,11 +36,11 @@ public class MessageError( /** * An error caused by the catch of a throwable. */ -public class ThrowableError( +public class ThrowableError( public val throwable: E ) : Error { override val message: String = throwable.message ?: throwable.toString() - override val cause: Error? = null + override val cause: Error? = throwable.cause?.let { ThrowableError(it) } } /** @@ -48,7 +48,7 @@ public class ThrowableError( */ public class ErrorException( public val error: Error -) : Exception(error.message) +) : Exception(error.message, error.cause?.let { ErrorException(it) }) public fun Try.getOrThrow(): S = when (this) { @@ -56,13 +56,21 @@ public fun Try.getOrThrow(): S = is Try.Failure -> throw Exception("Try was excepted to contain a success.") } -//FIXME: to improve +public class FilesystemError( + override val cause: Error? = null +) : Error { + + override val message: String = + "An unexpected error occurred on the filesystem." +} + +// FIXME: to improve @InternalReadiumApi public fun Timber.Forest.e(error: Error, message: String? = null) { - Timber.e(Exception(error.message), message) + e(Exception(error.message), message) } @InternalReadiumApi public fun Timber.Forest.w(error: Error, message: String? = null) { - Timber.w(Exception(error.message), message) + w(Exception(error.message), message) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/NetworkError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/NetworkError.kt new file mode 100644 index 0000000000..a659940367 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/NetworkError.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util + +public sealed class NetworkError( + public override val message: String, + public override val cause: Error? = null +) : Error { + + /** Equivalent to a 400 HTTP error. */ + public class BadRequest(cause: Error? = null) : + NetworkError("Invalid request which can't be processed", cause) { + + public constructor(message: String) : this(MessageError(message)) + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + /** + * Equivalent to a 503 HTTP error. + * + * Used when the source can't be reached, e.g. no Internet connection, or an issue with the + * file system. Usually this is a temporary error. + */ + public class Unavailable(cause: Error? = null) : + NetworkError("The resource is currently unavailable, please try again later.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + /** + * The Internet connection appears to be offline. + */ + public class Offline(cause: Error? = null) : + NetworkError("The Internet connection appears to be offline.", cause) +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetError.kt index dbfc81e435..b9f5d4b660 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetError.kt @@ -1,6 +1,14 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + package org.readium.r2.shared.util.asset import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.FilesystemError +import org.readium.r2.shared.util.NetworkError import org.readium.r2.shared.util.ThrowableError /** @@ -47,12 +55,11 @@ public sealed class AssetError( public class Forbidden(cause: Error? = null) : AssetError("You are not allowed to open this publication.", cause) - /** - * The publication can't be opened at the moment, for example because of a networking error. - * This error is generally temporary, so the operation may be retried or postponed. - */ - public class Unavailable(cause: Error? = null) : - AssetError("The publication is not available at the moment.", cause) + public class Network(public override val cause: NetworkError) : + AssetError("A network error occurred.", cause) + + public class Filesystem(public override val cause: FilesystemError) : + AssetError("A filesystem error occurred.", cause) /** * The provided credentials are incorrect and we can't open the publication in a @@ -75,4 +82,4 @@ public sealed class AssetError( public constructor(exception: Exception) : this(ThrowableError(exception)) } -} \ No newline at end of file +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 71e7167e44..ed125471ec 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -21,6 +21,8 @@ import org.readium.r2.shared.extensions.queryProjection import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Either import org.readium.r2.shared.util.Error as SharedError +import org.readium.r2.shared.util.FilesystemError +import org.readium.r2.shared.util.NetworkError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url @@ -111,13 +113,11 @@ public class AssetRetriever( public constructor(url: AbsoluteUrl, exception: Exception) : this(url, ThrowableError(exception)) } + public class Network(public override val cause: NetworkError) : + Error("A network error occurred.", cause) - public class Unavailable(cause: SharedError) : - Error("Asset seems not to be available at the moment.", cause) { - - public constructor(exception: Exception) : - this(ThrowableError(exception)) - } + public class Filesystem(public override val cause: FilesystemError) : + Error("A filesystem error occurred.", cause) public class OutOfMemory(error: OutOfMemoryError) : Error( @@ -235,29 +235,19 @@ public class AssetRetriever( private fun ResourceError.wrap(url: AbsoluteUrl): Error = when (this) { is ResourceError.Forbidden -> - Error.Forbidden( - url, - this - ) + Error.Forbidden(url, this) is ResourceError.NotFound -> - Error.InvalidAsset( - this - ) - is ResourceError.Unavailable, ResourceError.Offline -> - Error.Unavailable( - this - ) + Error.InvalidAsset(this) + is ResourceError.Network -> + Error.Network(cause) is ResourceError.OutOfMemory -> - Error.OutOfMemory( - cause.throwable - ) + Error.OutOfMemory(cause.throwable) is ResourceError.Other -> - Error.Unknown( - this - ) - else -> Error.Unknown( - this - ) + Error.Unknown(this) + is ResourceError.InvalidContent -> + Error.InvalidAsset(this) + is ResourceError.Filesystem -> + Error.Unknown(this) } /* Sniff unknown assets */ diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index 88585b1568..d642590d09 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -25,7 +25,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.tryOr import org.readium.r2.shared.util.MessageError -import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.http.HttpError @@ -292,7 +291,8 @@ public class AndroidDownloadManager internal constructor( } else { Try.failure( DownloadManager.Error.FileSystemError( - MessageError("Failed to rename the downloaded file.")) + MessageError("Failed to rename the downloaded file.") + ) ) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt index 92963ccfcf..f52b36b0af 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt @@ -87,7 +87,7 @@ public class ForegroundDownloadManager( } .onFailure { error -> forEachListener(id) { - onDownloadFailed(id, DownloadManager.Error.HttpError(error)) + onDownloadFailed(id, DownloadManager.Error.HttpError(error)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index c788678184..dbdf75b53b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -8,6 +8,7 @@ import org.readium.r2.shared.extensions.read import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.NetworkError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.io.CountingInputStream @@ -36,7 +37,7 @@ public class HttpResource( return if (contentLength != null) { Try.success(contentLength) } else { - Try.failure(ResourceError.Unavailable()) + Try.failure(ResourceError.Other(UnsupportedOperationException())) } } @@ -119,15 +120,15 @@ public class HttpResource( private fun ResourceError.Companion.wrapHttp(e: HttpError): ResourceError = when (e.kind) { HttpError.Kind.MalformedRequest, HttpError.Kind.BadRequest, HttpError.Kind.MethodNotAllowed -> - ResourceError.BadRequest(cause = e) - HttpError.Kind.Timeout, HttpError.Kind.Offline, HttpError.Kind.TooManyRedirects -> - ResourceError.Unavailable(e) + ResourceError.Network(NetworkError.BadRequest(cause = e)) + HttpError.Kind.Timeout, HttpError.Kind.Offline -> + ResourceError.Network(NetworkError.Offline(e)) HttpError.Kind.Unauthorized, HttpError.Kind.Forbidden -> ResourceError.Forbidden(e) HttpError.Kind.NotFound -> ResourceError.NotFound(e) - HttpError.Kind.Cancelled -> - ResourceError.Unavailable(e) + HttpError.Kind.Cancelled, HttpError.Kind.TooManyRedirects -> + ResourceError.Other(e) HttpError.Kind.MalformedResponse, HttpError.Kind.ClientError, HttpError.Kind.ServerError, HttpError.Kind.Other -> ResourceError.Other(e) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt index 605e88fae7..db92ab5088 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt @@ -122,7 +122,7 @@ public class ContentResource internal constructor( return ResourceTry.catching { val stream = contentResolver.openInputStream(uri) ?: return Try.failure( - ResourceError.Unavailable( + ResourceError.Other( Exception("Content provider recently crashed.") ) ) @@ -140,7 +140,7 @@ public class ContentResource internal constructor( } catch (e: SecurityException) { failure(ResourceError.Forbidden(e)) } catch (e: IOException) { - failure(ResourceError.Unavailable(e)) + failure(ResourceError.Filesystem(e)) } catch (e: Exception) { failure(ResourceError.Other(e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt index ee54e79413..d14ca31edd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt @@ -7,7 +7,6 @@ package org.readium.r2.shared.util.resource import java.io.FileNotFoundException -import java.io.IOException import java.nio.channels.Channels import java.nio.channels.FileChannel import kotlinx.coroutines.Dispatchers @@ -75,12 +74,10 @@ internal class FileChannelResource( override suspend fun length(): ResourceTry { if (!::_length.isInitialized) { - _length = withContext(Dispatchers.IO) { - try { + ResourceTry.catching { + _length = withContext(Dispatchers.IO) { check(channel.isOpen) Try.success(channel.size()) - } catch (e: IOException) { - Try.failure(ResourceError.Unavailable(e)) } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index f0e12eece2..e633498521 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -14,7 +14,9 @@ import org.json.JSONObject import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.FilesystemError import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.NetworkError import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try @@ -88,19 +90,12 @@ public sealed class ResourceError( override val cause: Error? = null ) : Error { - /** Equivalent to a 400 HTTP error. */ - public class BadRequest(cause: Error? = null) : - ResourceError("Invalid request which can't be processed", cause) { - - public constructor(exception: Exception) : this(ThrowableError(exception)) - } - /** Equivalent to a 404 HTTP error. */ public class NotFound(cause: Error? = null) : - ResourceError("Resource not found", cause) { + ResourceError("Resource not found.", cause) { - public constructor(exception: Exception) : this(ThrowableError(exception)) - } + public constructor(exception: Exception) : this(ThrowableError(exception)) + } /** * Equivalent to a 403 HTTP error. @@ -110,25 +105,19 @@ public sealed class ResourceError( */ public class Forbidden(cause: Error? = null) : ResourceError("You are not allowed to access the resource.", cause) { - public constructor(exception: Exception) : this(ThrowableError(exception)) - } + public constructor(exception: Exception) : this(ThrowableError(exception)) + } - /** - * Equivalent to a 503 HTTP error. - * - * Used when the source can't be reached, e.g. no Internet connection, or an issue with the - * file system. Usually this is a temporary error. - */ - public class Unavailable(cause: Error? = null) : - ResourceError("The resource is currently unavailable, please try again later.", cause) { + public class Network(public override val cause: NetworkError) : + ResourceError("A network error occurred.", cause) - public constructor(exception: Exception) : this(ThrowableError(exception)) - } + public class Filesystem(public override val cause: FilesystemError) : + ResourceError("A filesystem error occurred.", cause) { - /** - * The Internet connection appears to be offline. - */ - public object Offline : ResourceError("The Internet connection appears to be offline.") + public constructor(exception: Exception) : this( + FilesystemError(ThrowableError(exception)) + ) + } /** * Equivalent to a 507 HTTP error. @@ -138,11 +127,15 @@ public sealed class ResourceError( public class OutOfMemory(override val cause: ThrowableError) : ResourceError("The resource is too large to be read on this device.", cause) { - public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) - } + public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) + } - public class InvalidContent(cause: Error?) - : ResourceError("Content seems invalid. ", cause) + public class InvalidContent(cause: Error?) : + ResourceError("Content seems invalid. ", cause) { + + public constructor(message: String) : this(MessageError(message)) + public constructor(exception: Exception) : this(ThrowableError(exception)) + } /** For any other error, such as HTTP 500. */ public class Other(cause: Error) : ResourceError("A service error occurred", cause) { @@ -169,21 +162,15 @@ public class FailureResource( "${javaClass.simpleName}($error)" } - -/** - * Maps the result with the given [transform] - * - * If the [transform] throws an [Exception], it is wrapped in a failure with Resource.Exception.Other. - */ - -@Deprecated("Catch exceptions yourself to the most suitable ResourceError.", level = DeprecationLevel.ERROR, +@Deprecated( + "Catch exceptions yourself to the most suitable ResourceError.", + level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("map(transform)") ) @Suppress("UnusedReceiverParameter") public fun ResourceTry.mapCatching(): ResourceTry = throw NotImplementedError() - public inline fun ResourceTry.flatMapCatching(transform: (value: S) -> ResourceTry): ResourceTry = flatMap { try { @@ -235,20 +222,19 @@ public suspend fun Resource.readAsString(charset: Charset? = null): ResourceTry< public suspend fun Resource.readAsJson(): ResourceTry = readAsString(charset = Charsets.UTF_8) .decode( - { JSONObject(it) }, + { JSONObject(it) }, { "Content doesn't seem to be valid JSON." } ) - /** * Reads the full content as an XML document. */ public suspend fun Resource.readAsXml(): ResourceTry = read() .decode( - { XmlParser().parse(ByteArrayInputStream(it)) }, + { XmlParser().parse(ByteArrayInputStream(it)) }, { "Content doesn't seem to be valid XML." } - ) + ) /** * Reads the full content as a [Bitmap]. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt index 532ee45b6d..b8d5634f8f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt @@ -160,7 +160,7 @@ internal class JavaZipContainer( Try.success(bytes) } } catch (e: IOException) { - Try.failure(ResourceError.Unavailable(e)) + Try.failure(ResourceError.Filesystem(e)) } catch (e: Exception) { Try.failure(ResourceError.Other(e)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt index 8c76456a14..67307acd40 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt @@ -6,12 +6,12 @@ package org.readium.r2.shared.util.resource.content -import org.readium.r2.shared.util.Error as SharedError import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jsoup.Jsoup import org.jsoup.parser.Parser import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.Error as SharedError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource @@ -78,6 +78,6 @@ public class HtmlResourceContentExtractor : ResourceContentExtractor { val body = Jsoup.parse(html).body().text() // Transform HTML entities into their actual characters. Parser.unescapeEntities(body, false) - } + } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index 7765244d28..bed32b3df1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -92,7 +92,7 @@ internal class ChannelZipContainer( } catch (e: ResourceChannel.ResourceException) { Try.failure(e.error) } catch (e: Exception) { - Try.failure(ResourceError.Other(e)) + Try.failure(ResourceError.InvalidContent(e)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ResourceChannel.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ResourceChannel.kt index 4b8271b24a..70695188fe 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ResourceChannel.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ResourceChannel.kt @@ -14,12 +14,10 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceError -import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.zip.jvm.ClosedChannelException import org.readium.r2.shared.util.zip.jvm.NonWritableChannelException import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel @@ -29,7 +27,7 @@ internal class ResourceChannel( ) : SeekableByteChannel { class ResourceException( - val error: ResourceError, + val error: ResourceError ) : IOException(error.message) private val coroutineScope: CoroutineScope = diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index 212cd6823f..0540ef237b 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -6,18 +6,16 @@ package org.readium.r2.streamer -import java.nio.charset.Charset -import org.json.JSONObject import org.readium.r2.shared.extensions.addPrefix -import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.AssetError +import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpContainer import org.readium.r2.shared.util.mediatype.FormatRegistry @@ -25,7 +23,10 @@ import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceContainer +import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.RoutingContainer +import org.readium.r2.shared.util.resource.readAsJson import org.readium.r2.streamer.parser.PublicationParser import timber.log.Timber @@ -69,12 +70,7 @@ internal class ParserAssetFactory( asset: Asset.Resource ): Try { val manifest = asset.resource.readAsRwpm() - .mapFailure { - AssetError.InvalidAsset( - "Failed to read the publication as a RWPM", - ThrowableError(it) - ) - } + .mapFailure { AssetError.InvalidAsset(it) } .getOrElse { return Try.failure(it) } val baseUrl = manifest.linkWithRel("self")?.href?.resolve() @@ -133,18 +129,18 @@ internal class ParserAssetFactory( ) } - private suspend fun Resource.readAsRwpm(): Try = - try { - val bytes = read().getOrThrow() - val string = String(bytes, Charset.defaultCharset()) - val json = JSONObject(string) - val manifest = Manifest.fromJSON( - json, - mediaTypeRetriever = mediaTypeRetriever - ) - ?: throw Exception("Failed to parse the RWPM Manifest") - Try.success(manifest) - } catch (e: Exception) { - Try.failure(e) - } + private suspend fun Resource.readAsRwpm(): ResourceTry = + readAsJson() + .flatMap { json -> + Manifest.fromJSON( + json, + mediaTypeRetriever = mediaTypeRetriever + )?.let { manifest -> + Try.success(manifest) + } ?: Try.failure( + ResourceError.InvalidContent( + MessageError("Failed to parse the RWPM Manifest.") + ) + ) + } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 6604bfe770..b7b97e8a68 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -13,6 +13,7 @@ import org.readium.r2.shared.publication.protection.AdeptFallbackContentProtecti import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.publication.protection.LcpFallbackContentProtection import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.DefaultHttpClient @@ -21,7 +22,6 @@ import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.pdf.PdfDocumentFactory -import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.streamer.parser.PublicationParser import org.readium.r2.streamer.parser.audio.AudioParser import org.readium.r2.streamer.parser.epub.EpubParser @@ -29,7 +29,7 @@ import org.readium.r2.streamer.parser.image.ImageParser import org.readium.r2.streamer.parser.pdf.PdfParser import org.readium.r2.streamer.parser.readium.ReadiumWebPubParser -internal typealias PublicationTry = Try +internal typealias AssetTry = Try /** * Opens a Publication using a list of parsers. @@ -125,7 +125,7 @@ public class PublicationFactory( * It can be used to modify the manifest, the root container or the list of service * factories of the [Publication]. * @param warnings Logger used to broadcast non-fatal parsing warnings. - * @return A [Publication] or a [Publication.OpenError] in case of failure. + * @return A [Publication] or a [AssetError] in case of failure. */ public suspend fun open( asset: Asset, @@ -134,7 +134,7 @@ public class PublicationFactory( allowUserInteraction: Boolean, onCreatePublication: Publication.Builder.() -> Unit = {}, warnings: WarningLogger? = null - ): PublicationTry { + ): AssetTry { val compositeOnCreatePublication: Publication.Builder.() -> Unit = { this@PublicationFactory.onCreatePublication(this) onCreatePublication(this) @@ -227,21 +227,18 @@ public class PublicationFactory( private fun wrapParserException(e: PublicationParser.Error): AssetError = when (e) { is PublicationParser.Error.UnsupportedFormat -> - AssetError.UnsupportedAsset("Cannot find a parser for this asset") - is PublicationParser.Error.ResourceReading -> - when (e.cause) { - is ResourceError.BadRequest, is ResourceError.Other -> - AssetError.Unknown(e) - is ResourceError.Forbidden -> - AssetError.Forbidden(e) - is ResourceError.NotFound -> - AssetError.InvalidAsset(e) - is ResourceError.OutOfMemory -> - AssetError.OutOfMemory(e) - is ResourceError.Unavailable, is ResourceError.Offline -> - AssetError.Unavailable(e) - } - is PublicationParser.Error.ParsingFailed -> + AssetError.UnsupportedAsset("Cannot find a parser for this asset.") + is PublicationParser.Error.InvalidAsset -> AssetError.InvalidAsset(e) + is PublicationParser.Error.Filesystem -> + AssetError.Filesystem(e.cause) + is PublicationParser.Error.Forbidden -> + AssetError.Forbidden(e.cause) + is PublicationParser.Error.Network -> + AssetError.Network(e.cause) + is PublicationParser.Error.Other -> + AssetError.Unknown(e) + is PublicationParser.Error.OutOfMemory -> + AssetError.OutOfMemory(e.cause) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt index 79ea19b01f..6b2de45f03 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt @@ -7,7 +7,10 @@ package org.readium.r2.streamer.parser import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.FilesystemError import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.NetworkError +import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType @@ -45,31 +48,63 @@ public interface PublicationParser { warnings: WarningLogger? = null ): Try - public sealed class Error : org.readium.r2.shared.util.Error { + public sealed class Error(public override val message: String) : + org.readium.r2.shared.util.Error { - public class UnsupportedFormat : Error() { - - override val message: String = - "Asset format not supported." + public class UnsupportedFormat : + Error("Asset format not supported.") { override val cause: org.readium.r2.shared.util.Error? = null } - public class ParsingFailed(override val cause: org.readium.r2.shared.util.Error?) : Error() { + public class InvalidAsset(override val cause: org.readium.r2.shared.util.Error?) : + Error("An error occurred while parsing the publication.") { public constructor(message: String) : this(MessageError(message)) + } + + public class Forbidden(public override val cause: org.readium.r2.shared.util.Error?) : + Error("Access to some content was forbidden.") + + public class Network(public override val cause: NetworkError) : + Error("A network error occurred.") + + public class Filesystem(public override val cause: FilesystemError) : + Error("A filesystem error occurred.") + + public class OutOfMemory(override val cause: ThrowableError) : + Error("The resource is too large to be read on this device.") { + + public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) + } + + /** For any other error, such as HTTP 500. */ + public class Other(public override val cause: org.readium.r2.shared.util.Error) : + Error("A service error occurred") { - override val message: String = - "An error occurred while parsing the publication." + public constructor(exception: Exception) : this(ThrowableError(exception)) } - public class ResourceReading( - override val cause: ResourceError - ) : Error() { + internal companion object { - override val message: String = - "An IO error occurred." + fun ResourceError.toParserError() = + when (this) { + is ResourceError.Filesystem -> + Filesystem(cause) + is ResourceError.Forbidden -> + Forbidden(cause) + is ResourceError.InvalidContent -> + InvalidAsset(this) + is ResourceError.Network -> + Network(cause) + is ResourceError.NotFound -> + InvalidAsset(this) + is ResourceError.Other -> + Other(this) + is ResourceError.OutOfMemory -> + OutOfMemory(cause) + } } } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index 44253061b0..99fc9ddb39 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -49,7 +49,7 @@ public class AudioParser : PublicationParser { if (readingOrder.isEmpty()) { return Try.failure( - PublicationParser.Error.ParsingFailed("No audio file found in the publication.") + PublicationParser.Error.InvalidAsset("No audio file found in the publication.") ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 42a1822eca..0265d63c55 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -26,6 +26,7 @@ import org.readium.r2.shared.util.resource.readAsXml import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.readAsXmlOrNull import org.readium.r2.streamer.parser.PublicationParser +import org.readium.r2.streamer.parser.PublicationParser.Error.Companion.toParserError import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref /** @@ -52,9 +53,9 @@ public class EpubParser( .getOrElse { return Try.failure(it) } val opfResource = asset.container.get(opfPath) val opfXmlDocument = opfResource.readAsXml() - .getOrElse { return Try.failure(PublicationParser.Error.ResourceReading(it)) } + .getOrElse { return Try.failure(it.toParserError()) } val packageDocument = PackageDocument.parse(opfXmlDocument, opfPath, mediaTypeRetriever) - ?: return Try.failure(PublicationParser.Error.ParsingFailed("Invalid OPF file.")) + ?: return Try.failure(PublicationParser.Error.InvalidAsset("Invalid OPF file.")) val manifest = ManifestAdapter( packageDocument = packageDocument, @@ -95,13 +96,13 @@ public class EpubParser( container .get(Url("META-INF/container.xml")!!) .use { it.readAsXml() } - .getOrElse { return Try.failure(PublicationParser.Error.ResourceReading(it)) } + .getOrElse { return Try.failure(it.toParserError()) } .getFirst("rootfiles", Namespaces.OPC) ?.getFirst("rootfile", Namespaces.OPC) ?.getAttr("full-path") ?.let { Url.fromEpubHref(it) } ?.let { Try.success(it) } - ?: Try.failure(PublicationParser.Error.ParsingFailed("Cannot successfully parse OPF.")) + ?: Try.failure(PublicationParser.Error.InvalidAsset("Cannot successfully parse OPF.")) private suspend fun parseEncryptionData(container: Container): Map = container.readAsXmlOrNull("META-INF/encryption.xml") diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt index dfba4e8482..aa1e51054d 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/NavigationDocumentParser.kt @@ -41,7 +41,7 @@ internal object NavigationDocumentParser { prefixMap: Map ): Pair, List>? { val typeAttr = nav.getAttrNs("type", Namespaces.OPS) ?: return null - val types = parseProperties(typeAttr).mapNotNull { + val types = parseProperties(typeAttr).map { resolveProperty( it, prefixMap, diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt index aa3920a03f..e193c91102 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt @@ -115,7 +115,7 @@ internal data class Itemref( val notLinear = element.getAttr("linear") == "no" val propAttr = element.getAttr("properties").orEmpty() val properties = parseProperties(propAttr) - .mapNotNull { resolveProperty(it, prefixMap, DEFAULT_VOCAB.ITEMREF) } + .map { resolveProperty(it, prefixMap, DEFAULT_VOCAB.ITEMREF) } return Itemref(idref, !notLinear, properties) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ResourceAdapter.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ResourceAdapter.kt index 0189652c6d..d1870c12d3 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ResourceAdapter.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ResourceAdapter.kt @@ -50,14 +50,14 @@ internal class ResourceAdapter( return Links(readingOrder, resources) } - /** Recursively find the ids of the fallback items in [items] */ + /** Recursively find the ids contained in fallback chains of items with [ids]. */ private fun computeIdsWithFallbacks(ids: List): Set { val fallbackIds: MutableSet = mutableSetOf() ids.forEach { fallbackIds.addAll(computeFallbackChain(it)) } return fallbackIds } - /** Compute the ids contained in the fallback chain of [item] */ + /** Compute the ids contained in the fallback chain of item with [id]. */ private fun computeFallbackChain(id: String): Set { // The termination has already been checked while computing links val ids: MutableSet = mutableSetOf() diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index f0ef161715..8f5b0c63f4 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -48,7 +48,7 @@ public class ImageParser : PublicationParser { if (readingOrder.isEmpty()) { return Try.failure( - PublicationParser.Error.ParsingFailed("No bitmap found in the publication.") + PublicationParser.Error.InvalidAsset("No bitmap found in the publication.") ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index 948b7d6a1d..c1c903a435 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -20,6 +20,7 @@ import org.readium.r2.shared.util.pdf.PdfDocumentFactory import org.readium.r2.shared.util.pdf.toLinks import org.readium.r2.streamer.extensions.toLink import org.readium.r2.streamer.parser.PublicationParser +import org.readium.r2.streamer.parser.PublicationParser.Error.Companion.toParserError /** * Parses a PDF file into a Readium [Publication]. @@ -43,12 +44,10 @@ public class PdfParser( val resource = asset.container.entries()?.firstOrNull() ?: return Try.failure( - PublicationParser.Error.ParsingFailed("No PDF found in the publication.") + PublicationParser.Error.InvalidAsset("No PDF found in the publication.") ) val document = pdfFactory.open(resource, password = null) - .getOrElse { - return Try.failure(PublicationParser.Error.ResourceReading(it)) - } + .getOrElse { return Try.failure(it.toParserError()) } val tableOfContents = document.outline.toLinks(resource.url) val manifest = Manifest( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt index af9f2b2bc4..dc93cf3a42 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt @@ -14,6 +14,7 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.PositionsService +import org.readium.r2.shared.util.e import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.pdf.PdfDocument diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index e8b222bb58..77b2b628d0 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -23,6 +23,7 @@ import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.pdf.PdfDocumentFactory import org.readium.r2.shared.util.resource.readAsJson import org.readium.r2.streamer.parser.PublicationParser +import org.readium.r2.streamer.parser.PublicationParser.Error.Companion.toParserError import org.readium.r2.streamer.parser.audio.AudioLocatorService /** @@ -45,14 +46,14 @@ public class ReadiumWebPubParser( val manifestJson = asset.container .get(Url("manifest.json")!!) .readAsJson() - .getOrElse { return Try.failure(PublicationParser.Error.ResourceReading(it)) } + .getOrElse { return Try.failure(it.toParserError()) } val manifest = Manifest.fromJSON( manifestJson, mediaTypeRetriever = mediaTypeRetriever ) ?: return Try.failure( - PublicationParser.Error.ParsingFailed("Failed to parse the RWPM Manifest") + PublicationParser.Error.InvalidAsset("Failed to parse the RWPM Manifest") ) // Checks the requirements from the LCPDF specification. @@ -61,7 +62,7 @@ public class ReadiumWebPubParser( if (asset.mediaType == MediaType.LCP_PROTECTED_PDF && (readingOrder.isEmpty() || !readingOrder.all { MediaType.PDF.matches(it.mediaType) }) ) { - return Try.failure(PublicationParser.Error.ParsingFailed("Invalid LCP Protected PDF.")) + return Try.failure(PublicationParser.Error.InvalidAsset("Invalid LCP Protected PDF.")) } val servicesBuilder = Publication.ServicesBuilder().apply { diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt index f27d469848..2610e322f9 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt @@ -15,6 +15,8 @@ import org.json.JSONObject import org.readium.r2.opds.OPDS1Parser import org.readium.r2.opds.OPDS2Parser import org.readium.r2.shared.opds.ParseData +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.http.HttpRequest @@ -56,9 +58,9 @@ class CatalogFeedListViewModel(application: Application) : AndroidViewModel(appl } } - private suspend fun parseURL(urlString: String): Try { + private suspend fun parseURL(urlString: String): Try { val url = Url(urlString) - ?: return Try.failure(IllegalArgumentException("Invalid URL")) + ?: return Try.failure(MessageError("Invalid URL")) return httpClient.fetchWithDecoder(HttpRequest(url.toString())) { val result = it.body diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index e9bbda64fd..8f022745fe 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -19,6 +19,7 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.toUrl import org.readium.r2.streamer.PublicationFactory import org.readium.r2.testapp.data.BookRepository @@ -139,7 +140,7 @@ class Bookshelf( val coverFile = coverStorage.storeCover(publication, coverUrl) .getOrElse { - return Try.failure(ImportError.StorageError(it)) + return Try.failure(ImportError.ResourceError(ResourceError.Filesystem(it))) } val id = bookRepository.insertBook( diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt index 8fef603d01..e90be10a37 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt @@ -43,8 +43,8 @@ sealed class ImportError( } } - class StorageError( - override val cause: Throwable + class ResourceError( + val error: org.readium.r2.shared.util.resource.ResourceError ) : ImportError(R.string.import_publication_unexpected_io_exception) class DownloadFailed( diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index 27421dd315..d1c60d7dfd 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -8,14 +8,18 @@ package org.readium.r2.testapp.domain import androidx.annotation.StringRes import org.readium.r2.shared.UserException -import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.testapp.R sealed class PublicationError(@StringRes userMessageId: Int) : UserException(userMessageId) { - class Unavailable(val error: Error) : PublicationError(R.string.publication_error_unavailable) + class Network(val error: Error) : PublicationError( + R.string.publication_error_network_unexpected + ) + + class Filesystem(val error: Error) : PublicationError(R.string.publication_error_filesystem) class NotFound(val error: Error) : PublicationError(R.string.publication_error_not_found) @@ -57,12 +61,14 @@ sealed class PublicationError(@StringRes userMessageId: Int) : UserException(use OutOfMemory(error) is AssetError.InvalidAsset -> InvalidPublication(error) - is AssetError.Unavailable -> - Unavailable(error) is AssetError.Unknown -> Unexpected(error) is AssetError.UnsupportedAsset -> UnsupportedAsset(error) + is AssetError.Filesystem -> + Filesystem(error.cause) + is AssetError.Network -> + Network(error.cause) } operator fun invoke(error: AssetRetriever.Error): PublicationError = @@ -79,10 +85,12 @@ sealed class PublicationError(@StringRes userMessageId: Int) : UserException(use OutOfMemory(error) is AssetRetriever.Error.SchemeNotSupported -> SchemeNotSupported(error) - is AssetRetriever.Error.Unavailable -> - Unavailable(error) is AssetRetriever.Error.Unknown -> Unexpected(error) + is AssetRetriever.Error.Filesystem -> + Filesystem(error.cause) + is AssetRetriever.Error.Network -> + Filesystem(error.cause) } } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index 4ca75256c3..4686de8589 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -22,11 +22,13 @@ import org.readium.r2.shared.publication.opds.images 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.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.testapp.data.DownloadRepository import org.readium.r2.testapp.utils.extensions.copyToTempFile import org.readium.r2.testapp.utils.extensions.moveTo @@ -121,7 +123,7 @@ class LocalPublicationRetriever( coroutineScope.launch { val tempFile = uri.copyToTempFile(context, storageDir) .getOrElse { - listener.onError(ImportError.StorageError(it)) + listener.onError(ImportError.ResourceError(ResourceError.Filesystem(it))) return@launch } @@ -154,7 +156,7 @@ class LocalPublicationRetriever( } if ( - sourceAsset is org.readium.r2.shared.util.asset.Asset.Resource && + sourceAsset is Asset.Resource && sourceAsset.mediaType.matches(MediaType.LCP_LICENSE_DOCUMENT) ) { if (lcpPublicationRetriever == null) { @@ -176,7 +178,7 @@ class LocalPublicationRetriever( } catch (e: Exception) { Timber.d(e) tryOrNull { libraryFile.delete() } - listener.onError(ImportError.StorageError(e)) + listener.onError(ImportError.ResourceError(ResourceError.Filesystem(e))) return } @@ -331,14 +333,14 @@ class LcpPublicationRetriever( * Retrieves a publication protected with the given license. */ fun retrieve( - licenceAsset: org.readium.r2.shared.util.asset.Asset.Resource, + licenceAsset: Asset.Resource, licenceFile: File, coverUrl: AbsoluteUrl? ) { coroutineScope.launch { val license = licenceAsset.resource.read() .getOrElse { - listener.onError(ImportError.StorageError(it)) + listener.onError(ImportError.ResourceError(it)) return@launch } .let { diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt index 2b502cca62..0ace45df68 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt @@ -19,12 +19,12 @@ import org.readium.r2.navigator.epub.EpubNavigatorFactory import org.readium.r2.navigator.pdf.PdfNavigatorFactory import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.UserException -import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.allAreHtml import org.readium.r2.shared.publication.services.isRestricted import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.getOrElse import org.readium.r2.testapp.Readium diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt index 6f2aa91871..9caa148426 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt @@ -33,7 +33,7 @@ import org.readium.r2.shared.publication.services.search.search 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.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.testapp.Application import org.readium.r2.testapp.data.BookRepository import org.readium.r2.testapp.data.model.Highlight @@ -249,7 +249,7 @@ class ReaderViewModel( // Navigator.Listener - override fun onResourceLoadFailed(href: Url, error: Resource.Error) { + override fun onResourceLoadFailed(href: Url, error: ResourceError) { val message = when (error) { is ResourceError.OutOfMemory -> "The resource is too large to be rendered on this device: $href" else -> "Failed to render the resource: $href" diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt index 87b6496fc3..b9e91a94e4 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt @@ -228,8 +228,8 @@ class TtsViewModel private constructor( private fun onPlaybackError(error: TtsNavigator.State.Error) { val event = when (error) { is TtsNavigator.State.Error.ContentError -> { - Timber.e(error.exception) - Event.OnError(UserException(R.string.tts_error_other, cause = error.exception)) + Timber.e(error.error) + Event.OnError(UserException(R.string.tts_error_other, cause = error.error)) } is TtsNavigator.State.Error.EngineError<*> -> { val engineError = (error.error as AndroidTtsEngine.Error) diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/File.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/File.kt index b77f60a5ee..610dbf8148 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/File.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/File.kt @@ -11,22 +11,8 @@ package org.readium.r2.testapp.utils.extensions import java.io.File import java.io.FileFilter -import java.io.FileOutputStream -import java.net.URL import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withContext -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.flatMap -import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpError -import org.readium.r2.shared.util.http.HttpRequest -import org.readium.r2.shared.util.http.HttpResponse -import org.readium.r2.shared.util.http.HttpTry -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.testapp.BuildConfig -import timber.log.Timber suspend fun File.moveTo(target: File) = withContext(Dispatchers.IO) { if (this@moveTo.renameTo(target)) { @@ -46,46 +32,3 @@ fun File.listFilesSafely(filter: FileFilter? = null): List { val array: Array? = if (filter == null) listFiles() else listFiles(filter) return array?.toList() ?: emptyList() } - -suspend fun URL.downloadTo( - dest: File, - httpClient: HttpClient, - assetRetriever: AssetRetriever -): Try { - if (BuildConfig.DEBUG) Timber.i("download url $this") - return httpClient.download(HttpRequest(toString()), dest, assetRetriever) - .map { } -} - -private suspend fun HttpClient.download( - request: HttpRequest, - destination: File, - assetRetriever: AssetRetriever -): HttpTry = - try { - stream(request).flatMap { res -> - withContext(Dispatchers.IO) { - res.body.use { input -> - FileOutputStream(destination).use { output -> - val buf = ByteArray(1024 * 8) - var n: Int - var downloadedBytes = 0 - while (-1 != input.read(buf).also { n = it }) { - ensureActive() - downloadedBytes += n - output.write(buf, 0, n) - } - } - } - var response = res.response - if (response.mediaType.matches(MediaType.BINARY)) { - assetRetriever.retrieve(destination)?.mediaType?.let { - response = response.copy(mediaType = it) - } - } - Try.success(response) - } - } - } catch (e: Exception) { - Try.failure(HttpError.wrap(e)) - } diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/ErrorExt.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/ErrorExt.kt index f28d28c188..a2a097cdd8 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/ErrorExt.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/ErrorExt.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.util.ThrowableError * Convenience function to get the description of an error with its cause. */ fun Error.toDebugDescription(context: Context): String = - if (this is ThrowableError) { + if (this is ThrowableError<*>) { throwable.toDebugDescription(context) } else { var desc = "${javaClass.nameWithEnclosingClasses()}: $message" diff --git a/test-app/src/main/res/values/strings.xml b/test-app/src/main/res/values/strings.xml index 22a31f04d6..a57815dc0c 100644 --- a/test-app/src/main/res/values/strings.xml +++ b/test-app/src/main/res/values/strings.xml @@ -100,6 +100,8 @@ Asset format is not supported Publication has not been found. + An unexpected network error occurred. + An unexpected filesystem error occurred. Publication is temporarily unavailable. Provided credentials were incorrect You are not allowed to open this publication @@ -108,18 +110,6 @@ Publication looks corrupted. An unexpected error occurred. - This publication cannot be opened because it is protected with %1$s - This publication cannot be opened because it is protected with an unknown DRM - - Invalid request which can\'t be processed - Resource not found - You are not allowed to access the resource - The resource is currently unavailable, please try again later - The Internet connection appears to be offline - The resource is too large to be read on this device - An expected error occurred. - A service error occurred - Failed parsing Catalog No result Note From 0d433c4cff4b727b2b76c6e0580919dedc3f171e Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Sun, 29 Oct 2023 17:19:16 +0100 Subject: [PATCH 04/86] WIP --- .../readium/r2/lcp/LcpContentProtection.kt | 16 +- .../readium/r2/lcp/service/LicensesService.kt | 4 +- .../java/org/readium/r2/shared/util/Error.kt | 2 + .../org/readium/r2/shared/util/asset/Asset.kt | 19 +-- .../r2/shared/util/asset/AssetRetriever.kt | 152 +++++++++--------- .../readium/r2/shared/util/asset/AssetType.kt | 5 - .../shared/util/http/HttpResourceFactory.kt | 11 +- .../r2/shared/util/resource/BytesResource.kt | 11 ++ .../shared/util/resource/ContentResource.kt | 11 +- .../util/resource/DirectoryContainer.kt | 39 +---- .../r2/shared/util/resource/Factories.kt | 147 +++++------------ .../util/resource/FileChannelResource.kt | 1 + .../r2/shared/util/resource/FileResource.kt | 26 ++- .../util/resource/FileZipArchiveFactory.kt | 44 +++-- .../r2/shared/util/resource/Resource.kt | 18 +-- .../content/ResourceContentExtractor.kt | 15 -- .../util/zip/StreamingZipArchiveFactory.kt | 25 +-- .../java/org/readium/r2/testapp/Readium.kt | 6 - .../readium/r2/testapp/data/BookRepository.kt | 5 +- .../org/readium/r2/testapp/data/model/Book.kt | 16 +- .../readium/r2/testapp/domain/Bookshelf.kt | 24 +-- .../r2/testapp/domain/PublicationRetriever.kt | 4 +- .../readium/r2/testapp/domain/ReaderError.kt | 1 + .../readium/r2/testapp/domain/SearchError.kt | 27 ++++ .../org/readium/r2/testapp/domain/TtsError.kt | 30 ++++ .../r2/testapp/reader/ReaderRepository.kt | 2 +- .../r2/testapp/reader/ReaderViewModel.kt | 19 ++- .../r2/testapp/reader/tts/TtsViewModel.kt | 26 +-- test-app/src/main/res/values/strings.xml | 5 + 29 files changed, 310 insertions(+), 401 deletions(-) create mode 100644 test-app/src/main/java/org/readium/r2/testapp/domain/ReaderError.kt create mode 100644 test-app/src/main/java/org/readium/r2/testapp/domain/SearchError.kt create mode 100644 test-app/src/main/java/org/readium/r2/testapp/domain/TtsError.kt diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index cd5ff8ef09..b8542f15a7 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -18,9 +18,9 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.asset.AssetType import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.TransformingContainer @@ -139,14 +139,20 @@ internal class LcpContentProtection( assetRetriever.retrieve( url, mediaType = link.mediaType, - assetType = AssetType.Archive + containerType = if (link.mediaType.isZip) MediaType.ZIP else null ) .map { it as Asset.Container } .mapFailure { it.wrap() } } else { - (assetRetriever.retrieve(url) as? Asset.Container) - ?.let { Try.success(it) } - ?: Try.failure(AssetError.UnsupportedAsset()) + assetRetriever.retrieve(url) + .mapFailure { it.wrap() } + .flatMap { + if (it is Asset.Container) { + Try.success((it)) + } else { + Try.failure(AssetError.UnsupportedAsset()) + } + } } return asset.flatMap { createResultAsset(it, license) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index 7fd5d96ea9..c385125397 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -39,6 +39,7 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever @@ -57,7 +58,8 @@ internal class LicensesService( ) : LcpService, CoroutineScope by MainScope() { override suspend fun isLcpProtected(file: File): Boolean { - val asset = assetRetriever.retrieve(file) ?: return false + val asset = assetRetriever.retrieve(file) + .getOrElse { return false } return isLcpProtected(asset) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt index de988461fc..2b93cb2829 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt @@ -60,6 +60,8 @@ public class FilesystemError( override val cause: Error? = null ) : Error { + public constructor(exception: Exception) : this(ThrowableError(exception)) + override val message: String = "An unexpected error occurred on the filesystem." } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt index 2a54d423be..5e8c4fb5eb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt @@ -15,11 +15,6 @@ import org.readium.r2.shared.util.resource.Resource as SharedResource */ public sealed class Asset { - /** - * Type of the asset source. - */ - public abstract val assetType: AssetType - /** * Media type of the asset. */ @@ -41,9 +36,6 @@ public sealed class Asset { public val resource: SharedResource ) : Asset() { - override val assetType: AssetType = - AssetType.Resource - override suspend fun close() { resource.close() } @@ -53,22 +45,15 @@ public sealed class Asset { * A container asset providing access to several resources. * * @param mediaType Media type of the asset. - * @param exploded If this container is an exploded or packaged container. + * @param containerType Media type of the container. * @param container Opened container to access asset resources. */ public class Container( override val mediaType: MediaType, - exploded: Boolean, + public val containerType: MediaType, public val container: SharedContainer ) : Asset() { - override val assetType: AssetType = - if (exploded) { - AssetType.Directory - } else { - AssetType.Archive - } - override suspend fun close() { container.close() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index ed125471ec..025995a8ab 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -8,33 +8,30 @@ package org.readium.r2.shared.util.asset import android.content.ContentResolver import android.content.Context -import android.net.Uri import android.provider.MediaStore import java.io.File -import kotlin.Boolean import kotlin.Exception import kotlin.String import kotlin.let -import kotlin.run import kotlin.takeUnless import org.readium.r2.shared.extensions.queryProjection import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Either import org.readium.r2.shared.util.Error as SharedError import org.readium.r2.shared.util.FilesystemError +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.NetworkError 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.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.ArchiveFactory import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.ContainerFactory import org.readium.r2.shared.util.resource.ContainerMediaTypeSnifferContent -import org.readium.r2.shared.util.resource.DirectoryContainerFactory import org.readium.r2.shared.util.resource.FileResourceFactory import org.readium.r2.shared.util.resource.FileZipArchiveFactory import org.readium.r2.shared.util.resource.Resource @@ -50,7 +47,6 @@ import org.readium.r2.shared.util.toUrl public class AssetRetriever( private val mediaTypeRetriever: MediaTypeRetriever, private val resourceFactory: ResourceFactory, - private val containerFactory: ContainerFactory, private val archiveFactory: ArchiveFactory, private val contentResolver: ContentResolver ) { @@ -61,7 +57,6 @@ public class AssetRetriever( return AssetRetriever( mediaTypeRetriever = mediaTypeRetriever, resourceFactory = FileResourceFactory(mediaTypeRetriever), - containerFactory = DirectoryContainerFactory(mediaTypeRetriever), archiveFactory = FileZipArchiveFactory(mediaTypeRetriever), contentResolver = context.contentResolver ) @@ -75,7 +70,7 @@ public class AssetRetriever( public class SchemeNotSupported( public val scheme: Url.Scheme, - cause: SharedError? + cause: SharedError? = null ) : Error("Scheme $scheme is not supported.", cause) { public constructor(scheme: Url.Scheme, exception: Exception) : @@ -135,74 +130,44 @@ public class AssetRetriever( public suspend fun retrieve( url: AbsoluteUrl, mediaType: MediaType, - assetType: AssetType + containerType: MediaType? ): Try { - return when (assetType) { - AssetType.Archive -> - retrieveArchiveAsset(url, mediaType) - - AssetType.Directory -> - retrieveDirectoryAsset(url, mediaType) - - AssetType.Resource -> + return when (containerType) { + null -> retrieveResourceAsset(url, mediaType) + else -> + retrieveArchiveAsset(url, mediaType, containerType) } } private suspend fun retrieveArchiveAsset( url: AbsoluteUrl, - mediaType: MediaType + mediaType: MediaType, + containerType: MediaType ): Try { return retrieveResource(url, mediaType) .flatMap { resource: Resource -> - archiveFactory.create(resource, password = null, mediaType) + archiveFactory.create(resource, containerType, password = null) .mapFailure { error -> when (error) { is ArchiveFactory.Error.FormatNotSupported -> - Error.ArchiveFormatNotSupported( - error - ) - is ArchiveFactory.Error.ResourceReading -> + Error.ArchiveFormatNotSupported(error) + is ArchiveFactory.Error.ResourceError -> error.cause.wrap(url) is ArchiveFactory.Error.PasswordsNotSupported -> - Error.ArchiveFormatNotSupported( - error - ) + Error.ArchiveFormatNotSupported(error) } } } .map { container -> Asset.Container( mediaType, - exploded = false, - container + containerType, + container.container ) } } - private suspend fun retrieveDirectoryAsset( - url: AbsoluteUrl, - mediaType: MediaType - ): Try { - return containerFactory.create(url, mediaType) - .map { container -> - Asset.Container( - mediaType, - exploded = true, - container - ) - } - .mapFailure { error -> - when (error) { - is ContainerFactory.Error.SchemeNotSupported -> - Error.SchemeNotSupported( - error.scheme, - error - ) - } - } - } - private suspend fun retrieveResourceAsset( url: AbsoluteUrl, mediaType: MediaType @@ -224,10 +189,7 @@ public class AssetRetriever( .mapFailure { error -> when (error) { is ResourceFactory.Error.SchemeNotSupported -> - Error.SchemeNotSupported( - error.scheme, - error - ) + Error.SchemeNotSupported(error.scheme, error) } } } @@ -255,46 +217,82 @@ public class AssetRetriever( /** * Retrieves an asset from a local file. */ - public suspend fun retrieve(file: File): Asset? = + public suspend fun retrieve(file: File): Try = retrieve(file.toUrl()) - /** - * Retrieves an asset from a [Uri]. - */ - public suspend fun retrieve(uri: Uri): Asset? { - val url = uri.toUrl() - ?: return null - - return retrieve(url) - } - /** * Retrieves an asset from a [Url]. */ - public suspend fun retrieve(url: Url): Asset? { - if (url !is AbsoluteUrl) return null - + public suspend fun retrieve(url: AbsoluteUrl): Try { val resource = resourceFactory.create(url) - ?: run { - return containerFactory.create(url) - ?.let { retrieve(url, it, exploded = true) } + .getOrElse { + return Try.failure( + when (it) { + is ResourceFactory.Error.SchemeNotSupported -> + Error.SchemeNotSupported(it.scheme) + } + ) } - return archiveFactory.create(resource, password = null) - ?.let { retrieve(url, container = it, exploded = false) } - ?: retrieve(url, resource) + return when ( + val archive = archiveFactory.create( + resource, + archiveType = null, + password = null + ) + ) { + is Try.Failure -> + when (archive.value) { + is ArchiveFactory.Error.PasswordsNotSupported -> + return Try.failure(Error.ArchiveFormatNotSupported(archive.value)) + is ArchiveFactory.Error.ResourceError -> when (archive.value.cause) { + is ResourceError.Filesystem -> + return Try.failure(Error.Filesystem(archive.value.cause.cause)) + is ResourceError.Network -> + return Try.failure(Error.Network(archive.value.cause.cause)) + is ResourceError.Forbidden -> + return Try.failure(Error.Forbidden(url, archive.value)) + is ResourceError.NotFound -> + return Try.failure(Error.NotFound(url, archive.value)) + is ResourceError.Other -> + return Try.failure(Error.Unknown(archive.value)) + is ResourceError.OutOfMemory -> + return Try.failure( + Error.OutOfMemory(archive.value.cause.cause.throwable) + ) + is ResourceError.InvalidContent -> + return Try.failure(Error.InvalidAsset(archive.value)) + } + is ArchiveFactory.Error.FormatNotSupported -> + retrieve(url, resource) + ?.let { Try.success(it) } + ?: Try.failure( + Error.Unknown( + MessageError("Cannot determine media type.") + ) + ) + } + is Try.Success -> + retrieve(url, archive.value.container, archive.value.mediaType) + ?.let { Try.success(it) } + ?: Try.failure( + Error.Unknown( + MessageError("Cannot determine media type.") + ) + ) + } } private suspend fun retrieve( url: AbsoluteUrl, container: Container, - exploded: Boolean + containerType: MediaType ): Asset? { val mediaType = retrieveMediaType(url, Either(container)) ?: return null return Asset.Container( mediaType, - exploded = exploded, + containerType = containerType, container = container ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetType.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetType.kt index da1c0f9db8..852f5d7c35 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetType.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetType.kt @@ -15,11 +15,6 @@ public enum class AssetType(public val value: String) { */ Resource("resource"), - /** - * A directory container. - */ - Directory("directory"), - /** * An archive container. */ diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt index 6abab21b05..a78476f9a9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt @@ -16,18 +16,9 @@ public class HttpResourceFactory( private val httpClient: HttpClient ) : ResourceFactory { - override suspend fun create(url: AbsoluteUrl): Resource? { - if (!url.isHttp) { - return null - } - - // FIXME: should make a head request to check that url points to a resource - return HttpResource(httpClient, url) - } - override suspend fun create( url: AbsoluteUrl, - mediaType: MediaType + mediaType: MediaType? ): Try { if (!url.isHttp) { return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt index 5a13c838d7..4069a1e6b3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt @@ -7,7 +7,9 @@ package org.readium.r2.shared.util.resource import kotlinx.coroutines.runBlocking +import org.readium.r2.shared.extensions.coerceFirstNonNegative import org.readium.r2.shared.extensions.read +import org.readium.r2.shared.extensions.requireLengthFitInt import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrThrow @@ -40,6 +42,15 @@ public sealed class BaseBytesResource( return _bytes } + @Suppress("NAME_SHADOWING") + val range = range + .coerceFirstNonNegative() + .requireLengthFitInt() + + if (range.isEmpty()) { + return Try.success(ByteArray(0)) + } + return _bytes.map { it.read(range) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt index db92ab5088..673b832add 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt @@ -28,18 +28,9 @@ public class ContentResourceFactory( private val contentResolver: ContentResolver ) : ResourceFactory { - override suspend fun create(url: AbsoluteUrl): Resource? { - if (!url.isContent) { - return null - } - - // FIXME: should check if uri points t o a file - return ContentResource(url.toUri(), contentResolver) - } - override suspend fun create( url: AbsoluteUrl, - mediaType: MediaType + mediaType: MediaType? ): Try { if (!url.isContent) { return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index ebdebb69ee..51e64256cf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -10,19 +10,15 @@ import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.isParentOf -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.RelativeUrl -import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever /** * A file system directory as a [Container]. */ -internal class DirectoryContainer( +public class DirectoryContainer( private val root: File, private val mediaTypeRetriever: MediaTypeRetriever ) : Container { @@ -65,36 +61,3 @@ internal class DirectoryContainer( override suspend fun close() {} } - -public class DirectoryContainerFactory( - private val mediaTypeRetriever: MediaTypeRetriever -) : ContainerFactory { - - override suspend fun create(url: AbsoluteUrl): Container? { - val file = url.toFile() - ?: return null - - if (!tryOr(false) { file.isDirectory }) { - return null - } - - return create(file).getOrNull() - } - - override suspend fun create( - url: AbsoluteUrl, - mediaType: MediaType - ): Try { - val file = url.toFile() - ?: return Try.failure(ContainerFactory.Error.SchemeNotSupported(url.scheme)) - - return create(file) - } - - // Internal for testing purpose - internal suspend fun create(file: File): Try { - val container = DirectoryContainer(file, mediaTypeRetriever) - - return Try.success(container) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt index a9763ce305..0203e05628 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt @@ -16,72 +16,24 @@ import org.readium.r2.shared.util.tryRecover /** * A factory to read [Resource]s from [Url]s. - * - * An exception must be returned if the url scheme is not supported or - * the resource cannot be found. */ public interface ResourceFactory { - public sealed class Error : SharedError { + public sealed class Error( + override val message: String, + override val cause: SharedError? + ) : SharedError { public class SchemeNotSupported( public val scheme: Url.Scheme, - override val cause: SharedError? = null - ) : Error() { - - public constructor(scheme: Url.Scheme, exception: Exception) : this( - scheme, - ThrowableError(exception) - ) - - override val message: String = - "Url scheme $scheme is not supported." - } + cause: SharedError? = null + ) : Error("Url scheme $scheme is not supported.", cause) } - public suspend fun create(url: AbsoluteUrl): Resource? - - public suspend fun create(url: AbsoluteUrl, mediaType: MediaType): Try -} - -/** - * A factory to create [Container]s from [Url]s. - * - * An exception must be returned if the url scheme is not supported or - * the url doesn't seem to point to a container. - */ -public interface ContainerFactory { - - public sealed class Error : SharedError { - - public class SchemeNotSupported( - public val scheme: Url.Scheme, - override val cause: SharedError? = null - ) : Error() { - - public constructor(scheme: Url.Scheme, exception: Exception) : this( - scheme, - ThrowableError(exception) - ) - - override val message: String = - "Url scheme $scheme is not supported." - } - } - - /** - * Returns a [Container] to access the content if this factory claims that [url] points to - * a resource it can provide access to and null otherwise. - */ - public suspend fun create(url: AbsoluteUrl): Container? - - /** - * Tries to create a [Container] giving access to a [Url] known to point to a directory - * with the given [mediaType]. - * - * An error must be returned if the url scheme or media type is not supported. - */ - public suspend fun create(url: AbsoluteUrl, mediaType: MediaType): Try + public suspend fun create( + url: AbsoluteUrl, + mediaType: MediaType? = null + ): Try } /** @@ -109,24 +61,24 @@ public interface ArchiveFactory { public constructor(exception: Exception) : this(ThrowableError(exception)) } - public class ResourceReading( - override val cause: ResourceError + public class ResourceError( + override val cause: org.readium.r2.shared.util.resource.ResourceError ) : Error("An error occurred while attempting to read the resource.", cause) } - /** - * Returns a [Container] to access the archive content if this factory claims that [resource] is - * an archive that it supports and null otherwise. - */ - public suspend fun create(resource: Resource, password: String?): Container? + public data class Result( + val mediaType: MediaType, + val container: Container + ) /** - * Tries to create a [Container] from a [Resource] known to be an archive - * with the given [mediaType]. - * - * An error must be returned if the resource type, password or media type is not supported. + * Creates a new archive [Container] to access the entries of the given archive. */ - public suspend fun create(resource: Resource, password: String?, mediaType: MediaType): Try + public suspend fun create( + resource: Resource, + archiveType: MediaType? = null, + password: String? = null + ): Try } /** @@ -138,16 +90,19 @@ public class CompositeArchiveFactory( private val fallbackFactory: ArchiveFactory ) : ArchiveFactory { - override suspend fun create(resource: Resource, password: String?): Container? { - return primaryFactory.create(resource, password) - ?: fallbackFactory.create(resource, password) - } - - override suspend fun create(resource: Resource, password: String?, mediaType: MediaType): Try { - return primaryFactory.create(resource, password, mediaType) + override suspend fun create( + resource: Resource, + archiveType: MediaType?, + password: String? + ): Try { + return primaryFactory.create(resource, archiveType, password) .tryRecover { error -> - if (error is ArchiveFactory.Error.FormatNotSupported) { - fallbackFactory.create(resource, password, mediaType) + if ( + error is ArchiveFactory.Error.FormatNotSupported || + archiveType == null && error is ArchiveFactory.Error.ResourceError && + error.cause is ResourceError.InvalidContent + ) { + fallbackFactory.create(resource, archiveType) } else { Try.failure(error) } @@ -164,11 +119,10 @@ public class CompositeResourceFactory( private val fallbackFactory: ResourceFactory ) : ResourceFactory { - override suspend fun create(url: AbsoluteUrl): Resource? { - return primaryFactory.create(url) ?: fallbackFactory.create(url) - } - - override suspend fun create(url: AbsoluteUrl, mediaType: MediaType): Try { + override suspend fun create( + url: AbsoluteUrl, + mediaType: MediaType? + ): Try { return primaryFactory.create(url, mediaType) .tryRecover { error -> if (error is ResourceFactory.Error.SchemeNotSupported) { @@ -179,28 +133,3 @@ public class CompositeResourceFactory( } } } - -/** - * A composite container factory which first tries [primaryFactory] - * and falls back on [fallbackFactory] if it doesn't support the scheme. - */ -public class CompositeContainerFactory( - private val primaryFactory: ContainerFactory, - private val fallbackFactory: ContainerFactory -) : ContainerFactory { - - override suspend fun create(url: AbsoluteUrl): Container? { - return primaryFactory.create(url) ?: fallbackFactory.create(url) - } - - override suspend fun create(url: AbsoluteUrl, mediaType: MediaType): Try { - return primaryFactory.create(url, mediaType) - .tryRecover { error -> - if (error is ContainerFactory.Error.SchemeNotSupported) { - fallbackFactory.create(url, mediaType) - } else { - Try.failure(error) - } - } - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt index d14ca31edd..acda358390 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt @@ -16,6 +16,7 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.mediatype.MediaType +// TODO: to remove if the current approach of reading through shared storage proves good internal class FileChannelResource( override val source: AbsoluteUrl?, private val channel: FileChannel diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt index 00596a4b27..a5a352da77 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt @@ -8,6 +8,7 @@ package org.readium.r2.shared.util.resource import java.io.File import java.io.FileNotFoundException +import java.io.IOException import java.io.RandomAccessFile import java.nio.channels.Channels import kotlinx.coroutines.Dispatchers @@ -124,6 +125,8 @@ public class FileResource private constructor( failure(ResourceError.NotFound(e)) } catch (e: SecurityException) { failure(ResourceError.Forbidden(e)) + } catch (e: IOException) { + failure(ResourceError.Filesystem(e)) } catch (e: Exception) { failure(ResourceError.Other(e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. @@ -138,28 +141,17 @@ public class FileResourceFactory( private val mediaTypeRetriever: MediaTypeRetriever ) : ResourceFactory { - override suspend fun create(url: AbsoluteUrl): Resource? { - val file = url.toFile() - ?: return null - - try { - if (!file.isFile) { - return null - } - } catch (e: Exception) { - return null - } - - return FileResource(file, mediaTypeRetriever) - } - override suspend fun create( url: AbsoluteUrl, - mediaType: MediaType + mediaType: MediaType? ): Try { val file = url.toFile() ?: return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) - return Try.success(FileResource(file, mediaType)) + val resource = mediaType + ?.let { FileResource(file, mediaType) } + ?: FileResource(file, mediaTypeRetriever) + + return Try.success(resource) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt index cdfde76430..d5afce14bb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt @@ -7,12 +7,14 @@ package org.readium.r2.shared.util.resource import java.io.File +import java.io.IOException import java.util.zip.ZipException import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever @@ -23,36 +25,30 @@ public class FileZipArchiveFactory( private val mediaTypeRetriever: MediaTypeRetriever ) : ArchiveFactory { - override suspend fun create(resource: Resource, password: String?): Container? { - if (password != null) { - return null - } - - return resource.source?.toFile() - ?.let { open(it) } - ?.getOrNull() - } - override suspend fun create( resource: Resource, - password: String?, - mediaType: MediaType - ): Try { - if (password != null) { - return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) + archiveType: MediaType?, + password: String? + ): Try { + if (archiveType != null && !archiveType.matches(MediaType.ZIP)) { + return Try.failure(ArchiveFactory.Error.FormatNotSupported()) } - if (!mediaType.isZip) { - return Try.failure(ArchiveFactory.Error.FormatNotSupported()) + if (password != null) { + return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) } - return resource.source?.toFile() - ?.let { open(it) } - ?: Try.Failure( + val file = resource.source?.toFile() + ?: return Try.Failure( ArchiveFactory.Error.FormatNotSupported( MessageError("Resource not supported because file cannot be directly accessed.") ) ) + + val container = open(file) + .getOrElse { return Try.failure(it) } + + return Try.success(ArchiveFactory.Result(MediaType.ZIP, container)) } // Internal for testing purpose @@ -62,11 +58,13 @@ public class FileZipArchiveFactory( val archive = JavaZipContainer(ZipFile(file), file, mediaTypeRetriever) Try.success(archive) } catch (e: ZipException) { - Try.failure(ArchiveFactory.Error.FormatNotSupported(e)) + Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.InvalidContent(e))) } catch (e: SecurityException) { - Try.failure(ArchiveFactory.Error.ResourceReading(ResourceError.Forbidden(e))) + Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.Forbidden(e))) + } catch (e: IOException) { + Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.Filesystem(e))) } catch (e: Exception) { - Try.failure(ArchiveFactory.Error.ResourceReading(ResourceError.Other(e))) + Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.Other(e))) } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index e633498521..0a726af531 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -114,9 +114,7 @@ public sealed class ResourceError( public class Filesystem(public override val cause: FilesystemError) : ResourceError("A filesystem error occurred.", cause) { - public constructor(exception: Exception) : this( - FilesystemError(ThrowableError(exception)) - ) + public constructor(exception: Exception) : this(FilesystemError(exception)) } /** @@ -130,7 +128,7 @@ public sealed class ResourceError( public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) } - public class InvalidContent(cause: Error?) : + public class InvalidContent(cause: Error? = null) : ResourceError("Content seems invalid. ", cause) { public constructor(message: String) : this(MessageError(message)) @@ -138,8 +136,10 @@ public sealed class ResourceError( } /** For any other error, such as HTTP 500. */ - public class Other(cause: Error) : ResourceError("A service error occurred", cause) { + public class Other(cause: Error) : + ResourceError("An unclassified error occurred.", cause) { + public constructor(message: String) : this(MessageError(message)) public constructor(exception: Exception) : this(ThrowableError(exception)) } @@ -195,9 +195,7 @@ public fun ResourceTry.decode( ) } catch (e: Exception) { Try.failure( - ResourceError.InvalidContent( - MessageError(errorMessage()) - ) + ResourceError.InvalidContent(errorMessage()) ) } is Try.Failure -> @@ -245,8 +243,6 @@ public suspend fun Resource.readAsBitmap(): ResourceTry = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?.let { Try.success(it) } ?: Try.failure( - ResourceError.InvalidContent( - MessageError("Could not decode resource as a bitmap.") - ) + ResourceError.InvalidContent("Could not decode resource as a bitmap.") ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt index 67307acd40..c83b0a2cfe 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt @@ -11,11 +11,9 @@ import kotlinx.coroutines.withContext import org.jsoup.Jsoup import org.jsoup.parser.Parser import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.util.Error as SharedError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.readAsString @@ -38,19 +36,6 @@ public interface ResourceContentExtractor { */ public suspend fun createExtractor(resource: Resource): ResourceContentExtractor? } - - public sealed class Error( - public override val message: String - ) : SharedError { - - public class Resource( - override val cause: ResourceError? - ) : Error("An error occurred while attempting to read the resource.") - - public class Content( - override val cause: org.readium.r2.shared.util.Error? - ) : Error("Resource content doesn't match what was expected.") - } } @ExperimentalReadiumApi diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt index 069b5f89aa..5f7304c24c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt @@ -7,7 +7,6 @@ package org.readium.r2.shared.util.zip import java.io.File -import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever @@ -28,25 +27,13 @@ public class StreamingZipArchiveFactory( override suspend fun create( resource: Resource, + archiveType: MediaType?, password: String? - ): Container? { - if (password != null) { - return null + ): Try { + if (archiveType != null && !archiveType.matches(MediaType.ZIP)) { + return Try.failure(ArchiveFactory.Error.FormatNotSupported()) } - return tryOrNull { - val resourceChannel = ResourceChannel(resource) - val channel = wrapBaseChannel(resourceChannel) - val zipFile = ZipFile(channel, true) - ChannelZipContainer(zipFile, resource.source, mediaTypeRetriever) - } - } - - override suspend fun create( - resource: Resource, - password: String?, - mediaType: MediaType - ): Try { if (password != null) { return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) } @@ -56,9 +43,9 @@ public class StreamingZipArchiveFactory( val channel = wrapBaseChannel(resourceChannel) val zipFile = ZipFile(channel, true) val channelZip = ChannelZipContainer(zipFile, resource.source, mediaTypeRetriever) - Try.success(channelZip) + Try.success(ArchiveFactory.Result(MediaType.ZIP, channelZip)) } catch (e: ResourceChannel.ResourceException) { - Try.failure(ArchiveFactory.Error.ResourceReading(e.error)) + Try.failure(ArchiveFactory.Error.ResourceError(e.error)) } catch (e: Exception) { Try.failure(ArchiveFactory.Error.FormatNotSupported(e)) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index e6cedd4f5c..95d4a05071 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -25,7 +25,6 @@ import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.CompositeArchiveFactory import org.readium.r2.shared.util.resource.CompositeResourceFactory import org.readium.r2.shared.util.resource.ContentResourceFactory -import org.readium.r2.shared.util.resource.DirectoryContainerFactory import org.readium.r2.shared.util.resource.FileResourceFactory import org.readium.r2.shared.util.resource.FileZipArchiveFactory import org.readium.r2.shared.util.zip.StreamingZipArchiveFactory @@ -57,14 +56,9 @@ class Readium(context: Context) { ) ) - private val containerFactory = DirectoryContainerFactory( - mediaTypeRetriever - ) - val assetRetriever = AssetRetriever( mediaTypeRetriever, resourceFactory, - containerFactory, archiveFactory, context.contentResolver ) diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt index ffd023d456..631e4810ff 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt @@ -15,7 +15,6 @@ import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.indexOfFirstWithHref import org.readium.r2.shared.publication.protection.ContentProtection -import org.readium.r2.shared.util.asset.AssetType import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.testapp.data.db.BooksDao import org.readium.r2.testapp.data.model.Book @@ -82,7 +81,7 @@ class BookRepository( suspend fun insertBook( href: String, mediaType: MediaType, - assetType: AssetType, + containerType: MediaType?, drm: ContentProtection.Scheme?, publication: Publication, cover: File @@ -94,7 +93,7 @@ class BookRepository( href = href, identifier = publication.metadata.identifier ?: "", mediaType = mediaType, - assetType = assetType, + containerType = containerType, drm = drm, progression = "{}", cover = cover.path diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt b/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt index 2876898cc3..f7925ce05d 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt @@ -11,7 +11,6 @@ import androidx.room.Entity import androidx.room.PrimaryKey import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.asset.AssetType import org.readium.r2.shared.util.mediatype.MediaType @Entity(tableName = Book.TABLE_NAME) @@ -33,8 +32,8 @@ data class Book( val progression: String? = null, @ColumnInfo(name = MEDIA_TYPE) val rawMediaType: String, - @ColumnInfo(name = ASSET_TYPE) - val rawAssetType: String, + @ColumnInfo(name = CONTAINER_TYPE) + val rawContainerType: String, @ColumnInfo(name = DRM) val drm: String? = null, @ColumnInfo(name = COVER) @@ -50,7 +49,7 @@ data class Book( identifier: String, progression: String? = null, mediaType: MediaType, - assetType: AssetType, + containerType: MediaType?, drm: ContentProtection.Scheme?, cover: String ) : this( @@ -62,7 +61,7 @@ data class Book( identifier = identifier, progression = progression, rawMediaType = mediaType.toString(), - rawAssetType = assetType.value, + rawContainerType = containerType.toString(), drm = drm?.uri, cover = cover ) @@ -75,9 +74,8 @@ data class Book( val drmScheme: ContentProtection.Scheme? get() = drm?.let { ContentProtection.Scheme(it) } - val assetType: AssetType - get() = AssetType(rawAssetType) - ?: throw IllegalStateException("Invalid asset type $rawAssetType") + val containerType: MediaType? get() = + MediaType(rawContainerType) companion object { @@ -90,7 +88,7 @@ data class Book( const val IDENTIFIER = "identifier" const val PROGRESSION = "progression" const val MEDIA_TYPE = "media_type" - const val ASSET_TYPE = "asset_type" + const val CONTAINER_TYPE = "container_type" const val COVER = "cover" const val DRM = "drm" } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index 8f022745fe..00c1d22b09 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -16,7 +16,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetriever 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.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.ResourceError @@ -95,7 +95,7 @@ class Bookshelf( } fun addPublicationFromWeb( - url: Url + url: AbsoluteUrl ) { coroutineScope.launch { addBookFeedback(url) @@ -103,7 +103,7 @@ class Bookshelf( } fun addPublicationFromStorage( - url: Url + url: AbsoluteUrl ) { coroutineScope.launch { addBookFeedback(url) @@ -111,7 +111,7 @@ class Bookshelf( } private suspend fun addBookFeedback( - url: Url, + url: AbsoluteUrl, coverUrl: AbsoluteUrl? = null ) { addBook(url, coverUrl) @@ -120,14 +120,16 @@ class Bookshelf( } private suspend fun addBook( - url: Url, + url: AbsoluteUrl, coverUrl: AbsoluteUrl? = null ): Try { val asset = assetRetriever.retrieve(url) - ?: return Try.failure( - ImportError.PublicationError(PublicationError.UnsupportedAsset()) - ) + .getOrElse { + return Try.failure( + ImportError.PublicationError(PublicationError.UnsupportedAsset()) + ) + } val drmScheme = protectionRetriever.retrieve(asset) @@ -140,13 +142,15 @@ class Bookshelf( val coverFile = coverStorage.storeCover(publication, coverUrl) .getOrElse { - return Try.failure(ImportError.ResourceError(ResourceError.Filesystem(it))) + return Try.failure( + ImportError.ResourceError(ResourceError.Filesystem(it)) + ) } val id = bookRepository.insertBook( url.toString(), asset.mediaType, - asset.assetType, + (asset as? Asset.Container)?.containerType, drmScheme, publication, coverFile diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index 4686de8589..b6dc3540d5 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -148,9 +148,9 @@ class LocalPublicationRetriever( coverUrl: AbsoluteUrl? = null ) { val sourceAsset = assetRetriever.retrieve(tempFile) - ?: run { + .getOrElse { listener.onError( - ImportError.PublicationError(PublicationError.UnsupportedAsset()) + ImportError.PublicationError(PublicationError(it)) ) return } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ReaderError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ReaderError.kt new file mode 100644 index 0000000000..5dcee2b97e --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ReaderError.kt @@ -0,0 +1 @@ +package org.readium.r2.testapp.domain diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/SearchError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/SearchError.kt new file mode 100644 index 0000000000..665f8d08d4 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/SearchError.kt @@ -0,0 +1,27 @@ +package org.readium.r2.testapp.domain + +import androidx.annotation.StringRes +import org.readium.r2.shared.UserException +import org.readium.r2.shared.util.Error +import org.readium.r2.testapp.R + +sealed class SearchError(@StringRes userMessageId: Int) : UserException(userMessageId) { + + object PublicationNotSearchable : + SearchError(R.string.search_error_not_searchable) + + class BadQuery(val error: Error) : + SearchError(R.string.search_error_not_searchable) + + class ResourceError(val error: Error) : + SearchError(R.string.search_error_other) + + class NetworkError(val error: Error) : + SearchError(R.string.search_error_other) + + object Cancelled : + SearchError(R.string.search_error_cancelled) + + class Other(val error: Error) : + SearchError(R.string.search_error_other) +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/TtsError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/TtsError.kt new file mode 100644 index 0000000000..7db8b97144 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/TtsError.kt @@ -0,0 +1,30 @@ +package org.readium.r2.testapp.domain + +import androidx.annotation.StringRes +import org.readium.navigator.media.tts.TtsNavigator +import org.readium.navigator.media.tts.android.AndroidTtsEngine +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.UserException +import org.readium.r2.testapp.R + +@OptIn(ExperimentalReadiumApi::class) +sealed class TtsError(@StringRes userMessageId: Int) : UserException(userMessageId) { + + class ContentError(val error: TtsNavigator.State.Error.ContentError) : + TtsError(R.string.tts_error_other) + + sealed class EngineError(@StringRes userMessageId: Int) : TtsError(userMessageId) { + + class Network(val error: AndroidTtsEngine.Error.Network) : + EngineError(R.string.tts_error_network) + + class Other(val error: AndroidTtsEngine.Error) : + EngineError(R.string.tts_error_other) + } + + class ServiceError(val exception: Exception) : + TtsError(R.string.error_unexpected) + + class Initialization() : + TtsError(R.string.tts_error_initialization) +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt index 0ace45df68..1790de6dc7 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt @@ -112,7 +112,7 @@ class ReaderRepository( val asset = readium.assetRetriever.retrieve( book.url, book.mediaType, - book.assetType + book.containerType ).getOrElse { return Try.failure(OpeningError.PublicationError(it)) } val publication = readium.publicationFactory.open( diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt index 9caa148426..002c0c6f68 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt @@ -27,6 +27,7 @@ import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.LocatorCollection import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.services.search.SearchError import org.readium.r2.shared.publication.services.search.SearchIterator import org.readium.r2.shared.publication.services.search.SearchTry import org.readium.r2.shared.publication.services.search.search @@ -205,12 +206,28 @@ class ReaderViewModel( lastSearchQuery = query _searchLocators.value = emptyList() searchIterator = publication.search(query) - .onFailure { activityChannel.send(ActivityCommand.ToastError(it)) } + .onFailure { activityChannel.send(ActivityCommand.ToastError(it.wrap())) } .getOrNull() pagingSourceFactory.invalidate() searchChannel.send(SearchCommand.StartNewSearch) } + private fun SearchError.wrap(): org.readium.r2.testapp.domain.SearchError = + when (this) { + is SearchError.BadQuery -> + org.readium.r2.testapp.domain.SearchError.BadQuery(this) + SearchError.Cancelled -> + org.readium.r2.testapp.domain.SearchError.Cancelled + is SearchError.NetworkError -> + org.readium.r2.testapp.domain.SearchError.NetworkError(this) + is SearchError.Other -> + org.readium.r2.testapp.domain.SearchError.Other(this) + SearchError.PublicationNotSearchable -> + org.readium.r2.testapp.domain.SearchError.PublicationNotSearchable + is SearchError.ResourceError -> + org.readium.r2.testapp.domain.SearchError.ResourceError(this) + } + fun cancelSearch() = viewModelScope.launch { _searchLocators.value = emptyList() searchIterator?.close() diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt index b9e91a94e4..6bf615667f 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt @@ -22,11 +22,10 @@ import org.readium.navigator.media.tts.android.AndroidTtsSettings import org.readium.r2.navigator.Navigator import org.readium.r2.navigator.VisualNavigator import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Language -import org.readium.r2.testapp.R +import org.readium.r2.testapp.domain.TtsError import org.readium.r2.testapp.reader.MediaService import org.readium.r2.testapp.reader.MediaServiceFacade import org.readium.r2.testapp.reader.ReaderInitData @@ -79,7 +78,7 @@ class TtsViewModel private constructor( /** * Emitted when the [TtsNavigator] fails with an error. */ - class OnError(val error: UserException) : Event() + class OnError(val error: TtsError) : Event() /** * Emitted when the selected language cannot be played because it is missing voice data. @@ -182,8 +181,8 @@ class TtsViewModel private constructor( initialLocator = start, initialPreferences = preferencesManager.preferences.value ) ?: run { - val exception = UserException(R.string.tts_error_initialization) - _events.send(Event.OnError(exception)) + val error = TtsError.Initialization() + _events.send(Event.OnError(error)) return } @@ -191,7 +190,7 @@ class TtsViewModel private constructor( mediaServiceFacade.openSession(bookId, ttsNavigator) } catch (e: Exception) { ttsNavigator.close() - val exception = UserException(R.string.error_unexpected) + val exception = TtsError.ServiceError(e) _events.trySend(Event.OnError(exception)) launchJob = null return @@ -228,18 +227,21 @@ class TtsViewModel private constructor( private fun onPlaybackError(error: TtsNavigator.State.Error) { val event = when (error) { is TtsNavigator.State.Error.ContentError -> { - Timber.e(error.error) - Event.OnError(UserException(R.string.tts_error_other, cause = error.error)) + Event.OnError(TtsError.ContentError(error)) } is TtsNavigator.State.Error.EngineError<*> -> { val engineError = (error.error as AndroidTtsEngine.Error) when (engineError) { is AndroidTtsEngine.Error.LanguageMissingData -> Event.OnMissingVoiceData(engineError.language) - AndroidTtsEngine.Error.Network -> - Event.OnError(UserException(R.string.tts_error_network)) - else -> - Event.OnError(UserException(R.string.tts_error_other)) + is AndroidTtsEngine.Error.Network -> { + val ttsError = TtsError.EngineError.Network(engineError) + Event.OnError(ttsError) + } + else -> { + val ttsError = TtsError.EngineError.Other(engineError) + Event.OnError(ttsError) + } }.also { Timber.e("Error type: $error") } } } diff --git a/test-app/src/main/res/values/strings.xml b/test-app/src/main/res/values/strings.xml index a57815dc0c..c55377e196 100644 --- a/test-app/src/main/res/values/strings.xml +++ b/test-app/src/main/res/values/strings.xml @@ -140,5 +140,10 @@ Stop Voice + This publication is not searchable + The search was cancelled + An error occurred while searching + + An unexpected error occurred. From d35bd5ce7fd5cabeb387ba70a2c57af971e57233 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Tue, 31 Oct 2023 15:13:54 +0100 Subject: [PATCH 05/86] WIP --- .../exoplayer/audio/ExoPlayerDataSource.kt | 4 +- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 4 +- .../org/readium/r2/lcp/LcpDecryptorTest.kt | 9 +- .../container/LcplResourceLicenseContainer.kt | 4 +- .../navigator/media2/ExoPlayerDataSource.kt | 4 +- .../readium/r2/navigator/R2BasicWebView.kt | 4 +- .../navigator/audio/PublicationDataSource.kt | 4 +- .../r2/shared/datasource/DataSource.kt | 32 ++ .../r2/shared/datasource/DataSourceDecoder.kt | 132 +++++ .../datasource/DataSourceInputStream.kt | 134 +++++ .../java/org/readium/r2/shared/util/Error.kt | 15 +- .../r2/shared/util/asset/AssetRetriever.kt | 101 ++-- .../util/mediatype/MediaTypeRetriever.kt | 51 +- .../shared/util/mediatype/MediaTypeSniffer.kt | 541 +++++++++++------- .../util/mediatype/MediaTypeSnifferContent.kt | 142 +++-- .../r2/shared/util/resource/BytesResource.kt | 4 +- .../r2/shared/util/resource/FileResource.kt | 19 +- .../r2/shared/util/resource/MediaTypeExt.kt | 98 +++- .../r2/shared/util/resource/Resource.kt | 73 --- .../util/resource/ResourceDataSource.kt | 71 +++ .../util/resource/ResourceInputStream.kt | 139 +---- .../r2/shared/util/resource/ZipContainer.kt | 20 +- .../r2/shared/util/zip/ChannelZipContainer.kt | 11 +- .../readium/r2/shared/util/zip/HttpChannel.kt | 8 +- .../shared/util/resource/ZipContainerTest.kt | 14 +- .../parser/epub/EpubDeobfuscatorTest.kt | 4 +- .../r2/testapp/search/SearchPagingSource.kt | 4 +- 27 files changed, 1073 insertions(+), 573 deletions(-) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSource.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceDecoder.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceInputStream.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceDataSource.kt diff --git a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt index 1d82911ec0..512af8b095 100644 --- a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt @@ -16,7 +16,7 @@ import androidx.media3.datasource.TransferListener import java.io.IOException import kotlinx.coroutines.runBlocking import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.buffered import org.readium.r2.shared.util.toUrl @@ -117,7 +117,7 @@ internal class ExoPlayerDataSource internal constructor( val data = runBlocking { openedResource.resource .read(range = openedResource.position until (openedResource.position + length)) - .getOrThrow() + .assertSuccess() } if (data.isEmpty()) { diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 868b2aa5bd..59108bc02c 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -19,7 +19,7 @@ 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.getOrElse -import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.FailureResource import org.readium.r2.shared.util.resource.Resource @@ -209,7 +209,7 @@ internal class LcpDecryptor( val rangeLength = if (lastBlockRead) { // use decrypted length to ensure range.last doesn't exceed decrypted length - 1 - range.last.coerceAtMost(length().getOrThrow() - 1) - range.first + 1 + range.last.coerceAtMost(length().assertSuccess() - 1) - range.first + 1 } else { // the last block won't be read, so there's no need to compute length range.last - range.first + 1 diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt index db8699b8cb..79814ae8b9 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt @@ -18,6 +18,7 @@ import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use import timber.log.Timber @@ -50,7 +51,7 @@ internal suspend fun checkLengthComputationIsCorrect(publication: Publication) { (publication.readingOrder + publication.resources) .forEach { link -> - val trueLength = publication.get(link).use { it.read().getOrThrow().size.toLong() } + val trueLength = publication.get(link).use { it.read().assertSuccess().size.toLong() } publication.get(link).use { resource -> resource.length() .onFailure { @@ -71,7 +72,7 @@ internal suspend fun checkAllResourcesAreReadableByChunks(publication: Publicati (publication.readingOrder + publication.resources) .forEach { link -> Timber.d("attempting to read ${link.href} by chunks ") - val groundTruth = publication.get(link).use { it.read() }.getOrThrow() + val groundTruth = publication.get(link).use { it.read() }.assertSuccess() for (chunkSize in listOf(4096L, 2050L)) { publication.get(link).use { resource -> resource.readByChunks(chunkSize, groundTruth).onFailure { @@ -91,8 +92,8 @@ internal suspend fun checkExceedingRangesAreAllowed(publication: Publication) { (publication.readingOrder + publication.resources) .forEach { link -> publication.get(link).use { resource -> - val length = resource.length().getOrThrow() - val fullTruth = resource.read().getOrThrow() + val length = resource.length().assertSuccess() + val fullTruth = resource.read().assertSuccess() for ( range in listOf( 0 until length + 100, diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt index d3d947a0c7..442cd638e5 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt @@ -11,7 +11,7 @@ package org.readium.r2.lcp.license.container import kotlinx.coroutines.runBlocking import org.readium.r2.lcp.LcpException -import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.resource.Resource /** @@ -22,7 +22,7 @@ internal class LcplResourceLicenseContainer(private val resource: Resource) : Li override fun read(): ByteArray = try { - runBlocking { resource.read().getOrThrow() } + runBlocking { resource.read().assertSuccess() } } catch (e: Exception) { throw LcpException.Container.OpenFailed } diff --git a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt index e29a6d677a..08f544e012 100644 --- a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt +++ b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt @@ -19,7 +19,7 @@ import com.google.android.exoplayer2.upstream.TransferListener import java.io.IOException import kotlinx.coroutines.runBlocking import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.buffered import org.readium.r2.shared.util.toUrl @@ -119,7 +119,7 @@ public class ExoPlayerDataSource internal constructor(private val publication: P val data = runBlocking { openedResource.resource .read(range = openedResource.position until (openedResource.position + length)) - .getOrThrow() + .assertSuccess() } if (data.isEmpty()) { diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt index 822a3f7d54..9ad58800ac 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt @@ -47,7 +47,7 @@ import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.readAsString import org.readium.r2.shared.util.toUrl @@ -352,7 +352,7 @@ internal open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebV ?.use { res -> res.readAsString() .map { Jsoup.parse(it) } - .getOrThrow() + .assertSuccess() } ?.select("#$id") ?.first()?.html() diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt index db8a19a90c..f4ddbc6067 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt @@ -19,7 +19,7 @@ import com.google.android.exoplayer2.upstream.TransferListener import java.io.IOException import kotlinx.coroutines.runBlocking import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.buffered import org.readium.r2.shared.util.toUrl @@ -119,7 +119,7 @@ internal class PublicationDataSource(private val publication: Publication) : Bas val data = runBlocking { openedResource.resource .read(range = openedResource.position until (openedResource.position + length)) - .getOrThrow() + .assertSuccess() } if (data.isEmpty()) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSource.kt b/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSource.kt new file mode 100644 index 0000000000..9c5765fdba --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSource.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.datasource + +import org.readium.r2.shared.util.SuspendingCloseable +import org.readium.r2.shared.util.Try + +/** + * Acts as a proxy to an actual data source by handling read access. + */ +internal interface DataSource : SuspendingCloseable { + + /** + * Returns data length from metadata if available, or calculated from reading the bytes otherwise. + * + * This value must be treated as a hint, as it might not reflect the actual bytes length. To get + * the real length, you need to read the whole resource. + */ + suspend fun length(): Try + + /** + * Reads the bytes at the given range. + * + * When [range] is null, the whole content is returned. Out-of-range indexes are clamped to the + * available length automatically. + */ + suspend fun read(range: LongRange? = null): Try +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceDecoder.kt b/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceDecoder.kt new file mode 100644 index 0000000000..2396e99932 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceDecoder.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.datasource + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import java.io.ByteArrayInputStream +import java.nio.charset.Charset +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.shared.util.xml.XmlParser + +internal sealed class DecoderError( + override val message: String +): Error { + + class DataSourceError( + override val cause: E + ) : DecoderError("Data source error") + + class DecodingError( + override val cause: Error? + ) : DecoderError("Decoding Error") + +} + +internal suspend fun Try.decode( + block: (value: S) -> R, + wrapException: (Exception) -> Error +): Try> = + when (this) { + is Try.Success -> + try { + Try.success( + withContext(Dispatchers.Default) { + block(value) + } + ) + } catch (e: Exception) { + Try.failure(DecoderError.DecodingError(wrapException(e))) + } + is Try.Failure -> + Try.failure(DecoderError.DataSourceError(value)) + } + +internal suspend fun Try>.decode( + block: (value: S) -> R, + wrapException: (Exception) -> Error +): Try> = + when (this) { + is Try.Success -> + try { + Try.success( + withContext(Dispatchers.Default) { + block(value) + } + ) + } catch (e: Exception) { + Try.failure(DecoderError.DecodingError(wrapException(e))) + } + is Try.Failure -> + Try.failure(value) + } +/** + * Content as plain text. + * + * It will extract the charset parameter from the media type hints to figure out an encoding. + * Otherwise, fallback on UTF-8. + */ +internal suspend fun DataSource.readAsString( + charset: Charset = Charsets.UTF_8 +): Try> = + read().decode( + { String(it, charset = charset) }, + { MessageError("Content is not a valid $charset string.", ThrowableError(it)) } + ) + +/** Content as an XML document. */ +internal suspend fun DataSource.readAsXml(): Try> = + read().decode( + { XmlParser().parse(ByteArrayInputStream(it)) }, + { MessageError("Content is not a valid XML document.", ThrowableError(it)) } + ) + +/** + * Content parsed from JSON. + */ +internal suspend fun DataSource.readAsJson(): Try> = + readAsString().decode( + { JSONObject(it) }, + { MessageError("Content is not valid JSON.", ThrowableError(it)) } + ) + +/** Readium Web Publication Manifest parsed from the content. */ +internal suspend fun DataSource.readAsRwpm(): Try> = + readAsJson().flatMap { json -> + Manifest.fromJSON(json) + ?.let { Try.success(it) } + ?: Try.failure( + DecoderError.DecodingError( + MessageError("Content is not a valid RPWM.") + ) + ) + } + +/** + * Reads the full content as a [Bitmap]. + */ +internal suspend fun DataSource.readAsBitmap(): Try> = + read() + .mapFailure { DecoderError.DataSourceError(it) } + .flatMap { bytes -> + BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + ?.let { Try.success(it) } + ?: Try.failure( + DecoderError.DecodingError( + MessageError("Could not decode resource as a bitmap.") + ) + ) + } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceInputStream.kt new file mode 100644 index 0000000000..ae3d1160c9 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceInputStream.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.datasource + +import java.io.IOException +import java.io.InputStream +import kotlinx.coroutines.runBlocking +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.getOrThrow + +/** + * Input stream reading through a [DataSource]. + * + * If you experience bad performances, consider wrapping the stream in a BufferedInputStream. This + * is particularly useful when streaming deflated ZIP entries. + */ +internal class DataSourceInputStream( + private val dataSource: DataSource, + private val wrapError: (E) -> IOException, + private val range: LongRange? = null +) : InputStream() { + + private var isClosed = false + + private val end: Long by lazy { + val resourceLength = + runBlocking { dataSource.length() } + .mapFailure { wrapError(it) } + .getOrThrow() + + if (range == null) { + resourceLength + } else { + kotlin.math.min(resourceLength, range.last + 1) + } + } + + /** Current position in the resource. */ + private var position: Long = range?.start ?: 0 + + /** + * The currently marked position in the stream. Defaults to 0. + */ + private var mark: Long = range?.start ?: 0 + + override fun available(): Int { + checkNotClosed() + return (end - position).toInt() + } + + override fun skip(n: Long): Long = synchronized(this) { + checkNotClosed() + + val newPosition = (position + n).coerceAtMost(end) + val skipped = newPosition - position + position = newPosition + skipped + } + + override fun read(): Int = synchronized(this) { + checkNotClosed() + + if (available() <= 0) { + return -1 + } + + val bytes = runBlocking { dataSource.read(position until (position + 1)) + .mapFailure { wrapError(it) } + .getOrThrow() } + position += 1 + return bytes.first().toUByte().toInt() + } + + override fun read(b: ByteArray, off: Int, len: Int): Int = synchronized(this) { + checkNotClosed() + + if (available() <= 0) { + return -1 + } + + val bytesToRead = len.coerceAtMost(available()) + val bytes = runBlocking { dataSource.read(position until (position + bytesToRead)) + .mapFailure { wrapError(it) } + .getOrThrow() } + check(bytes.size <= bytesToRead) + bytes.copyInto( + destination = b, + destinationOffset = off, + startIndex = 0, + endIndex = bytes.size + ) + position += bytes.size + return bytes.size + } + + override fun markSupported(): Boolean = true + + override fun mark(readlimit: Int) { + synchronized(this) { + checkNotClosed() + mark = position + } + } + + override fun reset() { + synchronized(this) { + checkNotClosed() + position = mark + } + } + + /** + * Closes the underlying resource. + */ + override fun close() { + synchronized(this) { + if (isClosed) { + return + } + + isClosed = true + } + } + + private fun checkNotClosed() { + if (isClosed) { + throw IllegalStateException("InputStream is closed.") + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt index 2b93cb2829..d259670992 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt @@ -50,10 +50,19 @@ public class ErrorException( public val error: Error ) : Exception(error.message, error.cause?.let { ErrorException(it) }) -public fun Try.getOrThrow(): S = +public fun Try.assertSuccess(): S = when (this) { - is Try.Success -> value - is Try.Failure -> throw Exception("Try was excepted to contain a success.") + is Try.Success -> + value + is Try.Failure -> + throw IllegalStateException("Try was excepted to contain a success.") + } +public fun Try.assertSuccess(): S = + when (this) { + is Try.Success -> + value + is Try.Failure -> + throw IllegalStateException("Try was excepted to contain a success.", value) } public class FilesystemError( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 025995a8ab..b7af677f92 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -29,6 +29,8 @@ import org.readium.r2.shared.util.getOrElse 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 org.readium.r2.shared.util.mediatype.MediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferContentError import org.readium.r2.shared.util.resource.ArchiveFactory import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.ContainerMediaTypeSnifferContent @@ -209,7 +211,7 @@ public class AssetRetriever( is ResourceError.InvalidContent -> Error.InvalidAsset(this) is ResourceError.Filesystem -> - Error.Unknown(this) + Error.Filesystem(cause) } /* Sniff unknown assets */ @@ -245,72 +247,66 @@ public class AssetRetriever( when (archive.value) { is ArchiveFactory.Error.PasswordsNotSupported -> return Try.failure(Error.ArchiveFormatNotSupported(archive.value)) - is ArchiveFactory.Error.ResourceError -> when (archive.value.cause) { - is ResourceError.Filesystem -> - return Try.failure(Error.Filesystem(archive.value.cause.cause)) - is ResourceError.Network -> - return Try.failure(Error.Network(archive.value.cause.cause)) - is ResourceError.Forbidden -> - return Try.failure(Error.Forbidden(url, archive.value)) - is ResourceError.NotFound -> - return Try.failure(Error.NotFound(url, archive.value)) - is ResourceError.Other -> - return Try.failure(Error.Unknown(archive.value)) - is ResourceError.OutOfMemory -> - return Try.failure( - Error.OutOfMemory(archive.value.cause.cause.throwable) - ) - is ResourceError.InvalidContent -> - return Try.failure(Error.InvalidAsset(archive.value)) - } + is ArchiveFactory.Error.ResourceError -> + return Try.failure(archive.value.cause.wrap(url)) is ArchiveFactory.Error.FormatNotSupported -> retrieve(url, resource) - ?.let { Try.success(it) } - ?: Try.failure( - Error.Unknown( - MessageError("Cannot determine media type.") - ) - ) + .mapFailure {it.wrap(url) } } is Try.Success -> retrieve(url, archive.value.container, archive.value.mediaType) - ?.let { Try.success(it) } - ?: Try.failure( - Error.Unknown( - MessageError("Cannot determine media type.") - ) - ) + .mapFailure { it.wrap(url) } } } + private fun MediaTypeSniffer.Error.wrap(url: AbsoluteUrl) = when (this) { + is MediaTypeSniffer.Error.SourceError -> + when (cause) { + is MediaTypeSnifferContentError.Filesystem -> + Error.Filesystem(cause.cause) + is MediaTypeSnifferContentError.Forbidden -> + Error.Forbidden(url, cause.cause) + is MediaTypeSnifferContentError.Network -> + Error.Network(cause.cause) + is MediaTypeSnifferContentError.NotFound -> + Error.NotFound(url, cause.cause) + } + MediaTypeSniffer.Error.NotRecognized -> + Error.Unknown(MessageError("Cannot determine media type.")) + } + private suspend fun retrieve( url: AbsoluteUrl, container: Container, containerType: MediaType - ): Asset? { + ): Try { val mediaType = retrieveMediaType(url, Either(container)) - ?: return null - return Asset.Container( - mediaType, - containerType = containerType, - container = container - ) + .getOrElse { return Try.failure(it) } + + val asset = + Asset.Container( + mediaType, + containerType = containerType, + container = container + ) + + return Try.success(asset) } - private suspend fun retrieve(url: AbsoluteUrl, resource: Resource): Asset? { + private suspend fun retrieve(url: AbsoluteUrl, resource: Resource): Try { val mediaType = retrieveMediaType(url, Either(resource)) - ?: return null - return Asset.Resource( - mediaType, - resource = resource - ) + .getOrElse { return Try.failure(it) } + + val asset = Asset.Resource(mediaType, resource = resource) + + return Try.success(asset) } private suspend fun retrieveMediaType( url: AbsoluteUrl, asset: Either - ): MediaType? { - suspend fun retrieve(hints: MediaTypeHints): MediaType? = + ): Try { + suspend fun retrieve(hints: MediaTypeHints): Try = mediaTypeRetriever.retrieve( hints = hints, content = when (asset) { @@ -320,7 +316,12 @@ public class AssetRetriever( ) retrieve(MediaTypeHints(fileExtensions = listOfNotNull(url.extension))) - ?.let { return it } + .onSuccess { return Try.success(it) } + .onFailure { error -> + if (error is MediaTypeSniffer.Error.SourceError) { + return Try.failure(error) + } + } // Falls back on the [contentResolver] in case of content Uri. // Note: This is done after the heavy sniffing of the provided [sniffers], because @@ -337,9 +338,11 @@ public class AssetRetriever( ?.let { filename -> File(filename).extension } ) - retrieve(contentHints)?.let { return it } + retrieve(contentHints) + .getOrNull() + ?.let { return Try.success(it) } } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt index 8ebc1cb406..264be556ce 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt @@ -6,6 +6,8 @@ package org.readium.r2.shared.util.mediatype +import org.readium.r2.shared.util.Try + /** * Retrieves a canonical [MediaType] for the provided media type and file extension hints and/or * asset content. @@ -43,14 +45,18 @@ public class MediaTypeRetriever( * Retrieves a canonical [MediaType] for the provided media type and file extension [hints]. */ public fun retrieve(hints: MediaTypeHints): MediaType? { - sniffers.firstNotNullOfOrNull { it.sniffHints(hints) } - ?.let { return it } + for (sniffer in sniffers) { + sniffer.sniffHints(hints) + .getOrNull() + ?.let { return it } + } // Falls back on the system-wide registered media types using MimeTypeMap. // Note: This is done after the default sniffers, because otherwise it will detect // JSON, XML or ZIP formats before we have a chance of sniffing their content (for example, // for RWPM). SystemMediaTypeSniffer.sniffHints(hints) + .getOrNull() ?.let { return it } return hints.mediaTypes.firstOrNull() @@ -89,20 +95,45 @@ public class MediaTypeRetriever( public suspend fun retrieve( hints: MediaTypeHints = MediaTypeHints(), content: MediaTypeSnifferContent? = null - ): MediaType? { - sniffers.run { - firstNotNullOfOrNull { it.sniffHints(hints) } - ?: content?.let { firstNotNullOfOrNull { it.sniffContent(content) } } - }?.let { return it } + ): Try { + for (sniffer in sniffers) { + sniffer.sniffHints(hints) + .getOrNull() + ?.let { return Try.success(it) } + } + + if (content != null) { + for (sniffer in sniffers) { + sniffer.sniffContent(content) + .onSuccess { return Try.success(it) } + .onFailure { error -> + if (error is MediaTypeSniffer.Error.SourceError) { + return Try.failure(error) + } + } + } + } // Falls back on the system-wide registered media types using MimeTypeMap. // Note: This is done after the default sniffers, because otherwise it will detect // JSON, XML or ZIP formats before we have a chance of sniffing their content (for example, // for RWPM). - SystemMediaTypeSniffer.run { - sniffHints(hints) ?: content?.let { sniffContent(it) } - }?.let { return it } + SystemMediaTypeSniffer.sniffHints(hints) + .getOrNull() + ?.let { return Try.success(it) } + + if (content != null) { + SystemMediaTypeSniffer.sniffContent(content) + .onSuccess { return Try.success(it) } + .onFailure { error -> + if (error is MediaTypeSniffer.Error.SourceError) { + return Try.failure(error) + } + } + } return hints.mediaTypes.firstOrNull() + ?.let { Try.success(it) } + ?: Try.failure(MediaTypeSniffer.Error.NotRecognized) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index 16d6c2cd1a..f4883477eb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -7,31 +7,48 @@ package org.readium.r2.shared.util.mediatype import android.webkit.MimeTypeMap -import java.io.File import java.net.URLConnection import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject +import org.readium.r2.shared.datasource.DecoderError import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.RelativeUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse /** * Sniffs a [MediaType] from media type and file extension hints or asset content. */ public interface MediaTypeSniffer { + public sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { + public data object NotRecognized : + Error("Media type of resource could not be inferred.", null) + + public data class SourceError(override val cause: MediaTypeSnifferContentError) : + Error("An error occurred while trying to read content.", cause) + } + /** * Sniffs a [MediaType] from media type and file extension hints. */ - public fun sniffHints(hints: MediaTypeHints): MediaType? = null + public fun sniffHints(hints: MediaTypeHints): Try = + Try.failure(Error.NotRecognized) /** * Sniffs a [MediaType] from an asset [content]. */ - public suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? = null + public suspend fun sniffContent(content: MediaTypeSnifferContent): Try = + Try.failure(Error.NotRecognized) } /** @@ -40,87 +57,114 @@ public interface MediaTypeSniffer { * Must precede the HTML sniffer. */ public object XhtmlMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): MediaType? { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("xht", "xhtml") || hints.hasMediaType("application/xhtml+xml") ) { - return MediaType.XHTML + return Try.success(MediaType.XHTML) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? { + override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { if (content !is ResourceMediaTypeSnifferContent) { - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - content.contentAsXml()?.let { - if ( + content.contentAsXml() + .getOrElse { + when (it) { + is DecoderError.DataSourceError -> + return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + is DecoderError.DecodingError -> + null + } + } + ?.takeIf { it.name.lowercase(Locale.ROOT) == "html" && - it.namespace.lowercase(Locale.ROOT).contains("xhtml") - ) { - return MediaType.XHTML + it.namespace.lowercase(Locale.ROOT).contains("xhtml") + }?.let { + return Try.success(MediaType.XHTML) } - } - return null + + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } /** Sniffs an HTML document. */ public object HtmlMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): MediaType? { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("htm", "html") || hints.hasMediaType("text/html") ) { - return MediaType.HTML + return Try.success(MediaType.HTML) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? { + override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { if (content !is ResourceMediaTypeSnifferContent) { - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } // [contentAsXml] will fail if the HTML is not a proper XML document, hence the doctype check. - if ( - content.contentAsXml()?.name?.lowercase(Locale.ROOT) == "html" || - content.contentAsString()?.trimStart()?.take(15)?.lowercase() == "" - ) { - return MediaType.HTML - } - return null + content.contentAsXml() + .getOrElse { + when (it) { + is DecoderError.DataSourceError -> + return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + is DecoderError.DecodingError -> + null + } + } + ?.takeIf { it.name.lowercase(Locale.ROOT) == "html" } + ?.let { return Try.success(MediaType.HTML) } + + content.contentAsString() + .getOrElse { + when (it) { + is DecoderError.DataSourceError -> + return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + + is DecoderError.DecodingError -> + null + } + } + ?.takeIf { it.trimStart().take(15).lowercase() == "" } + ?.let { return Try.success(MediaType.HTML) } + + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } /** Sniffs an OPDS document. */ public object OpdsMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): MediaType? { + override fun sniffHints(hints: MediaTypeHints): Try { // OPDS 1 if (hints.hasMediaType("application/atom+xml;type=entry;profile=opds-catalog")) { - return MediaType.OPDS1_ENTRY + return Try.success(MediaType.OPDS1_ENTRY) } if (hints.hasMediaType("application/atom+xml;profile=opds-catalog;kind=navigation")) { - return MediaType.OPDS1_NAVIGATION_FEED + return Try.success(MediaType.OPDS1_NAVIGATION_FEED) } if (hints.hasMediaType("application/atom+xml;profile=opds-catalog;kind=acquisition")) { - return MediaType.OPDS1_ACQUISITION_FEED + return Try.success(MediaType.OPDS1_ACQUISITION_FEED) } if (hints.hasMediaType("application/atom+xml;profile=opds-catalog")) { - return MediaType.OPDS1 + return Try.success(MediaType.OPDS1) } // OPDS 2 if (hints.hasMediaType("application/opds+json")) { - return MediaType.OPDS2 + return Try.success(MediaType.OPDS2) } if (hints.hasMediaType("application/opds-publication+json")) { - return MediaType.OPDS2_PUBLICATION + return Try.success(MediaType.OPDS2_PUBLICATION) } // OPDS Authentication Document. @@ -128,273 +172,340 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { hints.hasMediaType("application/opds-authentication+json") || hints.hasMediaType("application/vnd.opds.authentication.v1.0+json") ) { - return MediaType.OPDS_AUTHENTICATION + return Try.success(MediaType.OPDS_AUTHENTICATION) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? { + override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { if (content !is ResourceMediaTypeSnifferContent) { - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } // OPDS 1 - content.contentAsXml()?.let { xml -> - if (xml.namespace == "http://www.w3.org/2005/Atom") { + content.contentAsXml() + .getOrElse { + when (it) { + is DecoderError.DataSourceError -> + return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + is DecoderError.DecodingError -> + null + } + } + ?.takeIf { it.namespace == "http://www.w3.org/2005/Atom" } + ?.let { xml -> if (xml.name == "feed") { - return MediaType.OPDS1 + return Try.success(MediaType.OPDS1) } else if (xml.name == "entry") { - return MediaType.OPDS1_ENTRY + return Try.success(MediaType.OPDS1_ENTRY) } } - } // OPDS 2 - content.contentAsRwpm()?.let { rwpm -> - if (rwpm.linkWithRel("self")?.mediaType?.matches("application/opds+json") == true) { - return MediaType.OPDS2 + content.contentAsRwpm() + .getOrElse { + when (it) { + is DecoderError.DataSourceError -> + return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + is DecoderError.DecodingError -> + null + } } + ?.let { rwpm -> + if (rwpm.linkWithRel("self")?.mediaType?.matches("application/opds+json") == true + ) { + return Try.success(MediaType.OPDS2) + } - /** - * Finds the first [Link] having a relation matching the given [predicate]. - */ - fun List.firstWithRelMatching(predicate: (String) -> Boolean): Link? = - firstOrNull { it.rels.any(predicate) } + /** + * Finds the first [Link] having a relation matching the given [predicate]. + */ + fun List.firstWithRelMatching(predicate: (String) -> Boolean): Link? = + firstOrNull { it.rels.any(predicate) } - if (rwpm.links.firstWithRelMatching { it.startsWith("http://opds-spec.org/acquisition") } != null) { - return MediaType.OPDS2_PUBLICATION + if (rwpm.links.firstWithRelMatching { it.startsWith("http://opds-spec.org/acquisition") } != null) { + return Try.success(MediaType.OPDS2_PUBLICATION) + } } - } // OPDS Authentication Document. - if (content.containsJsonKeys("id", "title", "authentication")) { - return MediaType.OPDS_AUTHENTICATION - } + content.containsJsonKeys("id", "title", "authentication") + .getOrElse { + when (it) { + is DecoderError.DataSourceError -> + return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + + is DecoderError.DecodingError -> + null + } + } + ?.takeIf { it } + ?.let { return Try.success(MediaType.OPDS_AUTHENTICATION) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } /** Sniffs an LCP License Document. */ public object LcpLicenseMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): MediaType? { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("lcpl") || hints.hasMediaType("application/vnd.readium.lcp.license.v1.0+json") ) { - return MediaType.LCP_LICENSE_DOCUMENT + return Try.success(MediaType.LCP_LICENSE_DOCUMENT) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? { + override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { if (content !is ResourceMediaTypeSnifferContent) { - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } + content.containsJsonKeys("id", "issued", "provider", "encryption") + .getOrElse { + when (it) { + is DecoderError.DataSourceError -> + return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) - if (content.containsJsonKeys("id", "issued", "provider", "encryption")) { - return MediaType.LCP_LICENSE_DOCUMENT - } - return null + is DecoderError.DecodingError -> + null + } + } + ?.takeIf { it } + ?.let { return Try.success(MediaType.LCP_LICENSE_DOCUMENT) } + + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } /** Sniffs a bitmap image. */ public object BitmapMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): MediaType? { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("avif") || hints.hasMediaType("image/avif") ) { - return MediaType.AVIF + return Try.success(MediaType.AVIF) } if ( hints.hasFileExtension("bmp", "dib") || hints.hasMediaType("image/bmp", "image/x-bmp") ) { - return MediaType.BMP + return Try.success(MediaType.BMP) } if ( hints.hasFileExtension("gif") || hints.hasMediaType("image/gif") ) { - return MediaType.GIF + return Try.success(MediaType.GIF) } if ( hints.hasFileExtension("jpg", "jpeg", "jpe", "jif", "jfif", "jfi") || hints.hasMediaType("image/jpeg") ) { - return MediaType.JPEG + return Try.success(MediaType.JPEG) } if ( hints.hasFileExtension("jxl") || hints.hasMediaType("image/jxl") ) { - return MediaType.JXL + return Try.success(MediaType.JXL) } if ( hints.hasFileExtension("png") || hints.hasMediaType("image/png") ) { - return MediaType.PNG + return Try.success(MediaType.PNG) } if ( hints.hasFileExtension("tiff", "tif") || hints.hasMediaType("image/tiff", "image/tiff-fx") ) { - return MediaType.TIFF + return Try.success(MediaType.TIFF) } if ( hints.hasFileExtension("webp") || hints.hasMediaType("image/webp") ) { - return MediaType.WEBP + return Try.success(MediaType.WEBP) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } /** Sniffs a Readium Web Manifest. */ public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): MediaType? { + override fun sniffHints(hints: MediaTypeHints): Try { if (hints.hasMediaType("application/audiobook+json")) { - return MediaType.READIUM_AUDIOBOOK_MANIFEST + return Try.success(MediaType.READIUM_AUDIOBOOK_MANIFEST) } if (hints.hasMediaType("application/divina+json")) { - return MediaType.DIVINA_MANIFEST + return Try.success(MediaType.DIVINA_MANIFEST) } if (hints.hasMediaType("application/webpub+json")) { - return MediaType.READIUM_WEBPUB_MANIFEST + return Try.success(MediaType.READIUM_WEBPUB_MANIFEST) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? { + override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { if (content !is ResourceMediaTypeSnifferContent) { - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } val manifest: Manifest = - content.contentAsRwpm() ?: return null + content.contentAsRwpm() + .getOrElse { + when (it) { + is DecoderError.DataSourceError -> + return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + + is DecoderError.DecodingError -> + null + } + } + ?: return Try.failure(MediaTypeSniffer.Error.NotRecognized) if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { - return MediaType.READIUM_AUDIOBOOK_MANIFEST + return Try.success(MediaType.READIUM_AUDIOBOOK_MANIFEST) } if (manifest.conformsTo(Publication.Profile.DIVINA)) { - return MediaType.DIVINA_MANIFEST + return Try.success(MediaType.DIVINA_MANIFEST) } if (manifest.linkWithRel("self")?.mediaType?.matches("application/webpub+json") == true) { - return MediaType.READIUM_WEBPUB_MANIFEST + return Try.success(MediaType.READIUM_WEBPUB_MANIFEST) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } /** Sniffs a Readium Web Publication, protected or not by LCP. */ public object WebPubMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): MediaType? { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("audiobook") || hints.hasMediaType("application/audiobook+zip") ) { - return MediaType.READIUM_AUDIOBOOK + return Try.success(MediaType.READIUM_AUDIOBOOK) } if ( hints.hasFileExtension("divina") || hints.hasMediaType("application/divina+zip") ) { - return MediaType.DIVINA + return Try.success(MediaType.DIVINA) } if ( hints.hasFileExtension("webpub") || hints.hasMediaType("application/webpub+zip") ) { - return MediaType.READIUM_WEBPUB + return Try.success(MediaType.READIUM_WEBPUB) } if ( hints.hasFileExtension("lcpa") || hints.hasMediaType("application/audiobook+lcp") ) { - return MediaType.LCP_PROTECTED_AUDIOBOOK + return Try.success(MediaType.LCP_PROTECTED_AUDIOBOOK) } if ( hints.hasFileExtension("lcpdf") || hints.hasMediaType("application/pdf+lcp") ) { - return MediaType.LCP_PROTECTED_PDF + return Try.success(MediaType.LCP_PROTECTED_PDF) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? { + override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { if (content !is ContainerMediaTypeSnifferContent) { - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } // Reads a RWPM from a manifest.json archive entry. - val manifest: Manifest? = - try { - content.read("manifest.json") - ?.let { - Manifest.fromJSON(JSONObject(String(it))) + val manifest: Manifest = + content.read(RelativeUrl("manifest.json")!!) + .getOrElse { error -> + when (error) { + is MediaTypeSnifferContentError.NotFound -> + null + else -> + return Try.failure(MediaTypeSniffer.Error.SourceError(error)) } - } catch (e: Exception) { - null - } - - if (manifest != null) { - val isLcpProtected = content.contains("license.lcpl") - - if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { - return if (isLcpProtected) { - MediaType.LCP_PROTECTED_AUDIOBOOK - } else { - MediaType.READIUM_AUDIOBOOK } + ?.let { tryOrNull { Manifest.fromJSON(JSONObject(String(it))) } } + ?: return Try.failure(MediaTypeSniffer.Error.NotRecognized) + + val isLcpProtected = content.checkContains(RelativeUrl("license.lcpl")!!) + .fold( + { true }, + { error -> + when (error) { + is MediaTypeSnifferContentError.NotFound -> + false + else -> + return Try.failure(MediaTypeSniffer.Error.SourceError(error)) + } + } + ) + + if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { + return if (isLcpProtected) { + Try.success(MediaType.LCP_PROTECTED_AUDIOBOOK) + } else { + Try.success(MediaType.READIUM_AUDIOBOOK) } - if (manifest.conformsTo(Publication.Profile.DIVINA)) { - return MediaType.DIVINA - } - if (isLcpProtected && manifest.conformsTo(Publication.Profile.PDF)) { - return MediaType.LCP_PROTECTED_PDF - } - if (manifest.linkWithRel("self")?.mediaType?.matches("application/webpub+json") == true) { - return MediaType.READIUM_WEBPUB - } + } + if (manifest.conformsTo(Publication.Profile.DIVINA)) { + return Try.success(MediaType.DIVINA) + } + if (isLcpProtected && manifest.conformsTo(Publication.Profile.PDF)) { + return Try.success(MediaType.LCP_PROTECTED_PDF) + } + if (manifest.linkWithRel("self")?.mediaType?.matches("application/webpub+json") == true) { + return Try.success(MediaType.READIUM_WEBPUB) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } /** Sniffs a W3C Web Publication Manifest. */ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { - override suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? { + override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { if (content !is ResourceMediaTypeSnifferContent) { - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. - val string = content.contentAsString() ?: "" + val string = content.contentAsString() + .getOrElse { + when (it) { + is DecoderError.DataSourceError -> + return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + + is DecoderError.DecodingError -> + null + } + } ?: "" if ( string.contains("@context") && string.contains("https://www.w3.org/ns/wp-context") ) { - return MediaType.W3C_WPUB_MANIFEST + return Try.success(MediaType.W3C_WPUB_MANIFEST) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } @@ -404,29 +515,37 @@ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { * Reference: https://www.w3.org/publishing/epub3/epub-ocf.html#sec-zip-container-mime */ public object EpubMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): MediaType? { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("epub") || hints.hasMediaType("application/epub+zip") ) { - return MediaType.EPUB + return Try.success(MediaType.EPUB) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? { + override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { if (content !is ContainerMediaTypeSnifferContent) { - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - val mimetype = content.read("mimetype") + val mimetype = content.read(RelativeUrl("mimetype")!!) + .getOrElse { error -> + when (error) { + is MediaTypeSnifferContentError.NotFound -> + null + else -> + return Try.failure(MediaTypeSniffer.Error.SourceError(error)) + } + } ?.let { String(it, charset = Charsets.US_ASCII).trim() } if (mimetype == "application/epub+zip") { - return MediaType.EPUB + return Try.success(MediaType.EPUB) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } @@ -438,39 +557,52 @@ public object EpubMediaTypeSniffer : MediaTypeSniffer { * - https://www.w3.org/TR/pub-manifest/ */ public object LpfMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): MediaType? { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("lpf") || hints.hasMediaType("application/lpf+zip") ) { - return MediaType.LPF + return Try.success(MediaType.LPF) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? { + override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { if (content !is ContainerMediaTypeSnifferContent) { - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - if (content.contains("index.html")) { - return MediaType.LPF - } + content.checkContains(RelativeUrl("index.html")!!) + .onSuccess { return Try.success(MediaType.LPF) } + .onFailure { error -> + when (error) { + is MediaTypeSnifferContentError.NotFound -> {} + else -> return Try.failure(MediaTypeSniffer.Error.SourceError(error)) + } + } // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. - content.read("publication.json") - ?.let { String(it) } + content.read(RelativeUrl("publication.json")!!) + .getOrElse { error -> + when (error) { + is MediaTypeSnifferContentError.NotFound -> + null + else -> + return Try.failure(MediaTypeSniffer.Error.SourceError(error)) + } + } + ?.let { tryOrNull { String(it) } } ?.let { manifest -> if ( manifest.contains("@context") && manifest.contains("https://www.w3.org/ns/pub-context") ) { - return MediaType.LPF + return Try.success(MediaType.LPF) } } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } @@ -524,7 +656,7 @@ public object ArchiveMediaTypeSniffer : MediaTypeSniffer { "zpl" ) - override fun sniffHints(hints: MediaTypeHints): MediaType? { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("cbz") || hints.hasMediaType( @@ -533,37 +665,36 @@ public object ArchiveMediaTypeSniffer : MediaTypeSniffer { "application/x-cbr" ) ) { - return MediaType.CBZ + return Try.success(MediaType.CBZ) } if (hints.hasFileExtension("zab")) { - return MediaType.ZAB + return Try.success(MediaType.ZAB) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? { + override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { if (content !is ContainerMediaTypeSnifferContent) { - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - fun isIgnored(file: File): Boolean = - file.name.startsWith(".") || file.name == "Thumbs.db" + fun isIgnored(url: Url): Boolean = + url.filename?.startsWith(".") == true || url.filename == "Thumbs.db" suspend fun archiveContainsOnlyExtensions(fileExtensions: List): Boolean = - content.entries()?.all { path -> - val file = File(path) - isIgnored(file) || fileExtensions.contains(file.extension.lowercase(Locale.ROOT)) + content.entries()?.all { url -> + isIgnored(url) || url.extension?.let { fileExtensions.contains(it.lowercase(Locale.ROOT)) } == true } ?: false if (archiveContainsOnlyExtensions(cbzExtensions)) { - return MediaType.CBZ + return Try.success(MediaType.CBZ) } if (archiveContainsOnlyExtensions(zabExtensions)) { - return MediaType.ZAB + return Try.success(MediaType.ZAB) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } @@ -573,49 +704,62 @@ public object ArchiveMediaTypeSniffer : MediaTypeSniffer { * Reference: https://www.loc.gov/preservation/digital/formats/fdd/fdd000123.shtml */ public object PdfMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): MediaType? { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("pdf") || hints.hasMediaType("application/pdf") ) { - return MediaType.PDF + return Try.success(MediaType.PDF) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? { + override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { if (content !is ResourceMediaTypeSnifferContent) { - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - if (content.read(0L until 5L)?.toString(Charsets.UTF_8) == "%PDF-") { - return MediaType.PDF - } + content.read(0L until 5L) + .getOrElse { error -> + return Try.failure(MediaTypeSniffer.Error.SourceError(error)) + } + .let { tryOrNull { it.toString(Charsets.UTF_8) } } + .takeIf { it == "%PDF-" } + ?.let { return Try.success(MediaType.PDF) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } /** Sniffs a JSON document. */ public object JsonMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): MediaType? { + override fun sniffHints(hints: MediaTypeHints): Try { if (hints.hasMediaType("application/problem+json")) { - return MediaType.JSON_PROBLEM_DETAILS + return Try.success(MediaType.JSON_PROBLEM_DETAILS) } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? { + override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { if (content !is ResourceMediaTypeSnifferContent) { - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - if (content.contentAsJson() != null) { - return MediaType.JSON - } - return null + content.contentAsJson() + .getOrElse { + when (it) { + is DecoderError.DataSourceError -> + return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + + is DecoderError.DecodingError -> + null + } + } + ?.let { return Try.success(MediaType.JSON) } + + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } } @@ -627,28 +771,39 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { private val mimetypes = tryOrNull { MimeTypeMap.getSingleton() } - override fun sniffHints(hints: MediaTypeHints): MediaType? { + override fun sniffHints(hints: MediaTypeHints): Try { for (mediaType in hints.mediaTypes) { - return sniffType(mediaType.toString()) ?: continue + sniffType(mediaType.toString()) + ?.let { return Try.success(it) } } for (extension in hints.fileExtensions) { - return sniffExtension(extension) ?: continue + sniffExtension(extension) + ?.let { return Try.success(it) } } - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): MediaType? { + override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { if (content !is ResourceMediaTypeSnifferContent) { - return null + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } - return withContext(Dispatchers.IO) { - content.contentAsStream() - .let { URLConnection.guessContentTypeFromStream(it) } - ?.let { sniffType(it) } - } + content.contentAsStream() + .use { + try { + withContext(Dispatchers.IO) { + URLConnection.guessContentTypeFromStream(it) + ?.let { sniffType(it) } + } + } catch (e: MediaTypeSnifferContentException) { + return Try.failure(MediaTypeSniffer.Error.SourceError(e.error)) + } + } + ?.let { return Try.success(it) } + + return Try.failure(MediaTypeSniffer.Error.NotRecognized) } private fun sniffType(type: String): MediaType? { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt index 217a36346a..de8ec47781 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt @@ -1,15 +1,25 @@ package org.readium.r2.shared.util.mediatype -import java.io.ByteArrayInputStream +import java.io.IOException import java.io.InputStream -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import org.json.JSONObject +import org.readium.r2.shared.datasource.DataSource +import org.readium.r2.shared.datasource.DataSourceInputStream +import org.readium.r2.shared.datasource.DecoderError +import org.readium.r2.shared.datasource.readAsJson +import org.readium.r2.shared.datasource.readAsRwpm +import org.readium.r2.shared.datasource.readAsString +import org.readium.r2.shared.datasource.readAsXml import org.readium.r2.shared.extensions.read -import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.FilesystemError +import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.NetworkError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.xml.ElementNode -import org.readium.r2.shared.util.xml.XmlParser /** * Provides read access to an asset content. @@ -27,7 +37,28 @@ public interface ResourceMediaTypeSnifferContent : MediaTypeSnifferContent { * It can be used to check a file signature, aka magic number. * See https://en.wikipedia.org/wiki/List_of_file_signatures */ - public suspend fun read(range: LongRange? = null): ByteArray? + public suspend fun read(range: LongRange? = null): Try + + public suspend fun length(): Try +} + +internal fun ResourceMediaTypeSnifferContent.asDataSource() = + ResourceMediaTypeSnifferContentDataSource(this) + +internal class ResourceMediaTypeSnifferContentDataSource( + private val resourceMediaTypeSnifferContent: ResourceMediaTypeSnifferContent +) : DataSource { + + override suspend fun length(): Try = + resourceMediaTypeSnifferContent.length() + + override suspend fun read(range: LongRange?): Try = + resourceMediaTypeSnifferContent.read(range) + + override suspend fun close() { + // ResourceMediaTypeSnifferContent doesn't own the resource. + // Do nothing. + } } /** @@ -36,38 +67,41 @@ public interface ResourceMediaTypeSnifferContent : MediaTypeSnifferContent { * It will extract the charset parameter from the media type hints to figure out an encoding. * Otherwise, fallback on UTF-8. */ -public suspend fun ResourceMediaTypeSnifferContent.contentAsString(): String? = - read()?.let { - tryOrNull { - withContext(Dispatchers.Default) { String(it) } - } - } +internal suspend fun ResourceMediaTypeSnifferContent.contentAsString() +: Try> = + asDataSource().readAsString() /** Content as an XML document. */ -public suspend fun ResourceMediaTypeSnifferContent.contentAsXml(): ElementNode? = - read()?.let { - tryOrNull { - withContext(Dispatchers.Default) { - XmlParser().parse(ByteArrayInputStream(it)) - } - } - } +internal suspend fun ResourceMediaTypeSnifferContent.contentAsXml() +: Try> = + asDataSource().readAsXml() /** * Content parsed from JSON. */ -public suspend fun ResourceMediaTypeSnifferContent.contentAsJson(): JSONObject? = - contentAsString()?.let { - tryOrNull { - withContext(Dispatchers.Default) { - JSONObject(it) - } - } - } +internal suspend fun ResourceMediaTypeSnifferContent.contentAsJson() +: Try> = + asDataSource().readAsJson() /** Readium Web Publication Manifest parsed from the content. */ -public suspend fun ResourceMediaTypeSnifferContent.contentAsRwpm(): Manifest? = - Manifest.fromJSON(contentAsJson()) +internal suspend fun ResourceMediaTypeSnifferContent.contentAsRwpm() +: Try> = + asDataSource().readAsRwpm() + +public sealed class MediaTypeSnifferContentError(override val message: String) : Error { + + public class NotFound(public override val cause: Error) : + MediaTypeSnifferContentError("Resource could not be found.") + + public class Forbidden(public override val cause: Error) : + MediaTypeSnifferContentError("You are not allowed to access this content.") + + public class Network(public override val cause: NetworkError) : + MediaTypeSnifferContentError("A network error occurred.") + + public class Filesystem(public override val cause: FilesystemError) : + MediaTypeSnifferContentError("An unexpected error occurred on filesystem.") +} /** * Raw bytes stream of the content. @@ -75,15 +109,22 @@ public suspend fun ResourceMediaTypeSnifferContent.contentAsRwpm(): Manifest? = * A byte stream can be useful when sniffers only need to read a few bytes at the beginning of * the file. */ -public suspend fun ResourceMediaTypeSnifferContent.contentAsStream(): InputStream = - ByteArrayInputStream(read() ?: ByteArray(0)) +internal fun ResourceMediaTypeSnifferContent.contentAsStream(): InputStream = + DataSourceInputStream(asDataSource(), ::MediaTypeSnifferContentException) + +internal class MediaTypeSnifferContentException( + val error: MediaTypeSnifferContentError +): IOException() /** * Returns whether the content is a JSON object containing all of the given root keys. */ -public suspend fun ResourceMediaTypeSnifferContent.containsJsonKeys(vararg keys: String): Boolean { - val json = contentAsJson() ?: return false - return json.keys().asSequence().toSet().containsAll(keys.toList()) +internal suspend fun ResourceMediaTypeSnifferContent.containsJsonKeys( + vararg keys: String +): Try> { + val json = contentAsJson() + .getOrElse { return Try.failure(it) } + return Try.success(json.keys().asSequence().toSet().containsAll(keys.toList())) } /** @@ -91,22 +132,32 @@ public suspend fun ResourceMediaTypeSnifferContent.containsJsonKeys(vararg keys: */ public interface ContainerMediaTypeSnifferContent : MediaTypeSnifferContent { /** - * Returns all the known entry paths in the container. + * Returns all the known entry urls in the container. */ - public suspend fun entries(): Set? + public suspend fun entries(): Set? /** - * Returns the entry data at the given [path] in this container. + * Returns the entry data at the given [url] in this container. */ - public suspend fun read(path: String, range: LongRange? = null): ByteArray? + public suspend fun read(url: Url, range: LongRange? = null): Try + + public suspend fun length(url: Url): Try } /** * Returns whether an entry exists in the container. */ -public suspend fun ContainerMediaTypeSnifferContent.contains(path: String): Boolean = - entries()?.contains(path) - ?: (read(path, range = 0L..1L) != null) +internal suspend fun ContainerMediaTypeSnifferContent.checkContains(url: Url): Try = + entries()?.contains(url) + ?.let { + if (it) Try.success(Unit) + else Try.failure( + MediaTypeSnifferContentError.NotFound( + MessageError("Container entry list doesn't contain $url.") + ) + ) } + ?: read(url, range = 0L..1L) + .map { } /** * A [ResourceMediaTypeSnifferContent] built from a raw byte array. @@ -126,6 +177,9 @@ public class BytesResourceMediaTypeSnifferContent( return _bytes } - override suspend fun read(range: LongRange?): ByteArray = - bytes().read(range) + override suspend fun read(range: LongRange?): Try = + Try.success(bytes().read(range)) + + override suspend fun length(): Try = + Try.success(bytes().size.toLong()) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt index 4069a1e6b3..2048b4f331 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt @@ -12,7 +12,7 @@ import org.readium.r2.shared.extensions.read import org.readium.r2.shared.extensions.requireLengthFitInt import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.mediatype.MediaType public sealed class BaseBytesResource( @@ -104,5 +104,5 @@ public class StringResource( this(source = url, mediaType = mediaType, properties = properties, { Try.success(string) }) override fun toString(): String = - "${javaClass.simpleName}(${runBlocking { read().getOrThrow().decodeToString() } }})" + "${javaClass.simpleName}(${runBlocking { read().assertSuccess().decodeToString() } }})" } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt index a5a352da77..6141357167 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt @@ -32,13 +32,11 @@ public class FileResource private constructor( private val mediaTypeRetriever: MediaTypeRetriever? ) : Resource { - public constructor(file: File, mediaType: MediaType) : this(file, mediaType, null) + public constructor(file: File, mediaType: MediaType) + : this(file, mediaType, null) - public constructor(file: File, mediaTypeRetriever: MediaTypeRetriever) : this( - file, - null, - mediaTypeRetriever - ) + public constructor(file: File, mediaTypeRetriever: MediaTypeRetriever) + : this(file, null, mediaTypeRetriever) private val randomAccessFile by lazy { try { @@ -53,14 +51,13 @@ public class FileResource private constructor( override suspend fun properties(): ResourceTry = ResourceTry.success(Resource.Properties()) - override suspend fun mediaType(): ResourceTry = Try.success( + override suspend fun mediaType(): ResourceTry = mediaType - ?: mediaTypeRetriever?.retrieve( + ?.let { Try.success(it) } + ?: mediaTypeRetriever!!.retrieve( hints = MediaTypeHints(fileExtension = file.extension), content = ResourceMediaTypeSnifferContent(this) - ) - ?: MediaType.BINARY - ) + ).toResourceTry() override suspend fun close() { withContext(Dispatchers.IO) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeExt.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeExt.kt index 234e81de85..5e504e02c0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeExt.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeExt.kt @@ -1,39 +1,109 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + package org.readium.r2.shared.util.resource +import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.getOrDefault +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.ContainerMediaTypeSnifferContent +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferContentError import org.readium.r2.shared.util.mediatype.ResourceMediaTypeSnifferContent +import org.readium.r2.shared.util.tryRecover public class ResourceMediaTypeSnifferContent( private val resource: Resource ) : ResourceMediaTypeSnifferContent { - override suspend fun read(range: LongRange?): ByteArray? = + override suspend fun read(range: LongRange?): Try = resource.safeRead(range) + .mapFailure { it.toMediaTypeSnifferContentError() } + + override suspend fun length(): Try = + resource.length() + .mapFailure { it.toMediaTypeSnifferContentError() } } public class ContainerMediaTypeSnifferContent( private val container: Container ) : ContainerMediaTypeSnifferContent { - override suspend fun entries(): Set? = - container.entries()?.mapNotNull { it.url.path }?.toSet() + override suspend fun entries(): Set? = + container.entries()?.map { it.url }?.toSet() - override suspend fun read(path: String, range: LongRange?): ByteArray? = - Url.fromDecodedPath(path)?.let { url -> - container.get(url).safeRead(range) - } -} + override suspend fun read(url: Url, range: LongRange?): Try = + container.get(url).safeRead(range) + .mapFailure { it.toMediaTypeSnifferContentError() } -private suspend fun Resource.safeRead(range: LongRange?): ByteArray? { + override suspend fun length(url: Url): Try = + container.get(url).length() + .mapFailure { it.toMediaTypeSnifferContentError() } + +} +private suspend fun Resource.safeRead(range: LongRange?): Try { try { + val length = length() + .getOrElse { return Try.failure(it) } + // We only read files smaller than 5MB to avoid an [OutOfMemoryError]. - if (range == null && length().getOrDefault(0) > 5 * 1000 * 1000) { - return null + if (range == null && length > 5 * 1000 * 1000) { + return Try.failure( + ResourceError.Other( + MessageError("Reading full content of big files is prevented.") + ) + ) } - return read(range).getOrNull() + return read(range) } catch (e: OutOfMemoryError) { - return null + return Try.failure(ResourceError.OutOfMemory(e)) } } + +internal fun ResourceError.toMediaTypeSnifferContentError() = + when (this) { + is ResourceError.Filesystem -> + MediaTypeSnifferContentError.Filesystem(cause) + is ResourceError.Forbidden -> + MediaTypeSnifferContentError.Forbidden(this) + is ResourceError.InvalidContent -> + is ResourceError.Network -> + MediaTypeSnifferContentError.Network(cause) + is ResourceError.NotFound -> + MediaTypeSnifferContentError.NotFound(this) + is ResourceError.Other -> + is ResourceError.OutOfMemory -> + } + +internal fun Try.toResourceTry(): ResourceTry = + tryRecover { + when (it) { + MediaTypeSniffer.Error.NotRecognized -> + Try.success(MediaType.BINARY) + else -> + Try.failure(it) + } + }.mapFailure { + when (it) { + MediaTypeSniffer.Error.NotRecognized -> + throw IllegalStateException() + is MediaTypeSniffer.Error.SourceError -> { + when (it.cause) { + is MediaTypeSnifferContentError.Filesystem -> + ResourceError.Filesystem(it.cause.cause) + is MediaTypeSnifferContentError.Forbidden -> + ResourceError.Forbidden(it.cause.cause) + is MediaTypeSnifferContentError.Network -> + ResourceError.Network(it.cause.cause) + is MediaTypeSnifferContentError.NotFound -> + ResourceError.NotFound(it.cause.cause) + } + } + } + + } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index 0a726af531..e80fb93ccf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -6,12 +6,6 @@ package org.readium.r2.shared.util.resource -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import java.io.ByteArrayInputStream -import java.nio.charset.Charset -import org.json.JSONObject -import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.FilesystemError @@ -22,8 +16,6 @@ import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.xml.ElementNode -import org.readium.r2.shared.util.xml.XmlParser public typealias ResourceTry = Try @@ -181,68 +173,3 @@ public inline fun ResourceTry.flatMapCatching(transform: (value: S) -> Try.failure(ResourceError.OutOfMemory(e)) } } - -@InternalReadiumApi -public fun ResourceTry.decode( - block: (value: S) -> R, - errorMessage: () -> String -): ResourceTry = - when (this) { - is Try.Success -> - try { - Try.success( - block(value) - ) - } catch (e: Exception) { - Try.failure( - ResourceError.InvalidContent(errorMessage()) - ) - } - is Try.Failure -> - Try.failure(value) - } - -/** - * Reads the full content as a [String]. - * - * If [charset] is null, then it falls back on UTF-8. - */ -public suspend fun Resource.readAsString(charset: Charset? = null): ResourceTry = - read() - .decode( - { String(it, charset = charset ?: Charsets.UTF_8) }, - { "Content doesn't seem to be a valid string." } - ) - -/** - * Reads the full content as a JSON object. - */ -public suspend fun Resource.readAsJson(): ResourceTry = - readAsString(charset = Charsets.UTF_8) - .decode( - { JSONObject(it) }, - { "Content doesn't seem to be valid JSON." } - ) - -/** - * Reads the full content as an XML document. - */ -public suspend fun Resource.readAsXml(): ResourceTry = - read() - .decode( - { XmlParser().parse(ByteArrayInputStream(it)) }, - { "Content doesn't seem to be valid XML." } - ) - -/** - * Reads the full content as a [Bitmap]. - */ -public suspend fun Resource.readAsBitmap(): ResourceTry = - read() - .flatMap { bytes -> - BitmapFactory.decodeByteArray(bytes, 0, bytes.size) - ?.let { Try.success(it) } - ?: Try.failure( - ResourceError.InvalidContent("Could not decode resource as a bitmap.") - ) - } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceDataSource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceDataSource.kt new file mode 100644 index 0000000000..fb71f12c0f --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceDataSource.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import android.graphics.Bitmap +import java.nio.charset.Charset +import org.json.JSONObject +import org.readium.r2.shared.datasource.DataSource +import org.readium.r2.shared.datasource.DecoderError +import org.readium.r2.shared.datasource.readAsBitmap +import org.readium.r2.shared.datasource.readAsJson +import org.readium.r2.shared.datasource.readAsString +import org.readium.r2.shared.datasource.readAsXml +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.xml.ElementNode + +private fun DecoderError.toResourceError() = + when (this) { + is DecoderError.DataSourceError -> + cause + is DecoderError.DecodingError -> + ResourceError.InvalidContent(cause) + } +/** + * Reads the full content as a [String]. + * + * If [charset] is null, then it falls back on UTF-8. + */ +public suspend fun Resource.readAsString(charset: Charset = Charsets.UTF_8): ResourceTry = + asDataSource().readAsString(charset).mapFailure{ it.toResourceError() } + +/** + * Reads the full content as a JSON object. + */ +public suspend fun Resource.readAsJson(): ResourceTry = + asDataSource().readAsJson().mapFailure{ it.toResourceError() } + +/** + * Reads the full content as an XML document. + */ +public suspend fun Resource.readAsXml(): ResourceTry = + asDataSource().readAsXml().mapFailure{ it.toResourceError() } + +/** + * Reads the full content as a [Bitmap]. + */ +public suspend fun Resource.readAsBitmap(): ResourceTry = + asDataSource().readAsBitmap().mapFailure { it.toResourceError() } + + +internal class ResourceDataSource( + private val resource: Resource +): DataSource { + + override suspend fun length(): Try = + resource.length() + + override suspend fun read(range: LongRange?): Try = + resource.read() + + override suspend fun close() { + resource.close() + } +} + +internal fun Resource.asDataSource() = + ResourceDataSource(this) \ No newline at end of file diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceInputStream.kt index 2236313478..a36e3550b5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceInputStream.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceInputStream.kt @@ -6,139 +6,28 @@ package org.readium.r2.shared.util.resource +import java.io.FilterInputStream import java.io.IOException -import java.io.InputStream -import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.datasource.DataSourceInputStream +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.resource.ResourceInputStream.ResourceException /** * Input stream reading a [Resource]'s content. * * If you experience bad performances, consider wrapping the stream in a BufferedInputStream. This * is particularly useful when streaming deflated ZIP entries. + * + * Raises [ResourceException]s when [ResourceError]s occur. */ -public class ResourceInputStream( - private val resource: Resource, - public val range: LongRange? = null -) : InputStream() { - - private var isClosed = false - - private val end: Long by lazy { - val resourceLength = try { - runBlocking { resource.length().getOrThrow() } - } catch (e: Exception) { - throw IOException("Can't get resource length", e) - } - - if (range == null) { - resourceLength - } else { - kotlin.math.min(resourceLength, range.last + 1) - } - } - - /** Current position in the resource. */ - private var position: Long = range?.start ?: 0 - - /** - * The currently marked position in the stream. Defaults to 0. - */ - private var mark: Long = range?.start ?: 0 - - @Throws(IOException::class) - override fun available(): Int { - checkNotClosed() - return (end - position).toInt() - } - - @Throws(IOException::class) - override fun skip(n: Long): Long = synchronized(this) { - checkNotClosed() - - val newPosition = (position + n).coerceAtMost(end) - val skipped = newPosition - position - position = newPosition - skipped - } - - @Throws(IOException::class) - override fun read(): Int = synchronized(this) { - checkNotClosed() - - if (available() <= 0) { - return -1 - } - - try { - val bytes = runBlocking { resource.read(position until (position + 1)).getOrThrow() } - position += 1 - return bytes.first().toUByte().toInt() - } catch (e: Exception) { - throw IOException("Can't read ResourceInputStream", e) - } - } - - @Throws(IOException::class) - override fun read(b: ByteArray, off: Int, len: Int): Int = synchronized(this) { - checkNotClosed() - - if (available() <= 0) { - return -1 - } - - try { - val bytesToRead = len.coerceAtMost(available()) - val bytes = runBlocking { resource.read(position until (position + bytesToRead)).getOrThrow() } - check(bytes.size <= bytesToRead) - bytes.copyInto( - destination = b, - destinationOffset = off, - startIndex = 0, - endIndex = bytes.size - ) - position += bytes.size - return bytes.size - } catch (e: Exception) { - throw IOException("Can't read ResourceInputStream", e) - } - } - - override fun markSupported(): Boolean = true - - @Throws(IOException::class) - override fun mark(readlimit: Int) { - synchronized(this) { - checkNotClosed() - mark = position - } - } - - @Throws(IOException::class) - override fun reset() { - synchronized(this) { - checkNotClosed() - position = mark - } - } - - /** - * Closes the underlying resource. - */ - override fun close() { - synchronized(this) { - if (isClosed) { - return - } +public class ResourceInputStream private constructor( + dataSourceInputStream: DataSourceInputStream +) : FilterInputStream(dataSourceInputStream) { - isClosed = true - runBlocking { resource.close() } - } - } + public constructor(resource: Resource, range: LongRange? = null) : + this(DataSourceInputStream(resource.asDataSource(), ::ResourceException, range)) - private fun checkNotClosed() { - if (isClosed) { - throw IOException("InputStream is closed.") - } - } + public class ResourceException( + public val error: ResourceError + ) : IOException(error.message, ErrorException(error)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt index b8d5634f8f..eb067ec0ad 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt @@ -93,12 +93,10 @@ internal class JavaZipContainer( override val source: AbsoluteUrl? = null override suspend fun mediaType(): ResourceTry = - Try.success( - mediaTypeRetriever.retrieve( - hints = MediaTypeHints(fileExtension = url.extension), - content = ResourceMediaTypeSnifferContent(this) - ) ?: MediaType.BINARY - ) + mediaTypeRetriever.retrieve( + hints = MediaTypeHints(fileExtension = url.extension), + content = ResourceMediaTypeSnifferContent(this) + ).toResourceTry() override suspend fun properties(): ResourceTry = Try.failure(ResourceError.NotFound()) @@ -118,12 +116,10 @@ internal class JavaZipContainer( override val source: AbsoluteUrl? = null override suspend fun mediaType(): ResourceTry = - Try.success( - mediaTypeRetriever.retrieve( - hints = MediaTypeHints(fileExtension = url.extension), - content = ResourceMediaTypeSnifferContent(this) - ) ?: MediaType.BINARY - ) + mediaTypeRetriever.retrieve( + hints = MediaTypeHints(fileExtension = url.extension), + content = ResourceMediaTypeSnifferContent(this) + ).toResourceTry() override suspend fun properties(): ResourceTry = ResourceTry.success( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index bed32b3df1..1b601aea78 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -27,6 +27,7 @@ import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.ResourceMediaTypeSnifferContent import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.archive +import org.readium.r2.shared.util.resource.toResourceTry import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipArchiveEntry import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile @@ -59,12 +60,10 @@ internal class ChannelZipContainer( ) override suspend fun mediaType(): ResourceTry = - Try.success( - mediaTypeRetriever.retrieve( - hints = MediaTypeHints(fileExtension = url.extension), - content = ResourceMediaTypeSnifferContent(this) - ) ?: MediaType.BINARY - ) + mediaTypeRetriever.retrieve( + hints = MediaTypeHints(fileExtension = url.extension), + content = ResourceMediaTypeSnifferContent(this) + ).toResourceTry() override suspend fun length(): ResourceTry = entry.size.takeUnless { it == -1L } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/HttpChannel.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/HttpChannel.kt index 91f3ac96b6..7768fbcf8e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/HttpChannel.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/HttpChannel.kt @@ -16,7 +16,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.extensions.readSafe import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest @@ -108,7 +108,7 @@ internal class HttpChannel( withContext(Dispatchers.IO) { val size = headResponse() .map { it.contentLength } - .getOrThrow() + .assertSuccess() ?: throw IOException("Server didn't provide content length.") if (position >= size) { @@ -120,7 +120,7 @@ internal class HttpChannel( val buffer = ByteArray(dst.remaining().coerceAtMost(available.toInt())) Timber.d("bufferSize ${buffer.size}") val read = stream(position) - .getOrThrow() + .assertSuccess() .readSafe(buffer) Timber.d("read $read") if (read != -1) { @@ -157,7 +157,7 @@ internal class HttpChannel( override fun size(): Long { return synchronized(lock) { runBlocking { headResponse() } - .getOrThrow() + .assertSuccess() .contentLength ?: throw IOException("Unknown file length.") } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt index f538bbbd13..b110efc128 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -19,7 +19,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.use @@ -90,14 +90,14 @@ class ZipContainerTest(val sut: suspend () -> Container) { @Test fun `Attempting to read a missing entry throws`(): Unit = runBlocking { sut().use { container -> - assertFails { container.get(Url("unknown")!!).read().getOrThrow() } + assertFails { container.get(Url("unknown")!!).read().assertSuccess() } } } @Test fun `Fully reading an entry works well`(): Unit = runBlocking { sut().use { container -> - val bytes = container.get(Url("mimetype")!!).read().getOrThrow() + val bytes = container.get(Url("mimetype")!!).read().assertSuccess() assertEquals("application/epub+zip", bytes.toString(StandardCharsets.UTF_8)) } } @@ -105,7 +105,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { @Test fun `Reading a range of an entry works well`(): Unit = runBlocking { sut().use { container -> - val bytes = container.get(Url("mimetype")!!).read(0..10L).getOrThrow() + val bytes = container.get(Url("mimetype")!!).read(0..10L).assertSuccess() assertEquals("application", bytes.toString(StandardCharsets.UTF_8)) assertEquals(11, bytes.size) } @@ -114,7 +114,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { @Test fun `Out of range indexes are clamped to the available length`(): Unit = runBlocking { sut().use { container -> - val bytes = container.get(Url("mimetype")!!).read(-5..60L).getOrThrow() + val bytes = container.get(Url("mimetype")!!).read(-5..60L).assertSuccess() assertEquals("application/epub+zip", bytes.toString(StandardCharsets.UTF_8)) assertEquals(20, bytes.size) } @@ -123,7 +123,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { @Test fun `Decreasing ranges are understood as empty ones`(): Unit = runBlocking { sut().use { container -> - val bytes = container.get(Url("mimetype")!!).read(60..20L).getOrThrow() + val bytes = container.get(Url("mimetype")!!).read(60..20L).assertSuccess() assertEquals("", bytes.toString(StandardCharsets.UTF_8)) assertEquals(0, bytes.size) } @@ -132,7 +132,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { @Test fun `Computing size works well`(): Unit = runBlocking { sut().use { container -> - val size = container.get(Url("mimetype")!!).length().getOrThrow() + val size = container.get(Url("mimetype")!!).length().assertSuccess() assertEquals(20L, size) } } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt index 5804a1e29f..eea8bf466e 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt @@ -15,7 +15,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.DirectoryContainerFactory import org.readium.r2.shared.util.resource.Resource @@ -76,7 +76,7 @@ class EpubDeobfuscatorTest { val deobfuscatedRes = deobfuscate( "/cut-cut.obf.woff", "http://www.idpf.org/2008/embedding" - ).read(20L until 40L).getOrThrow() + ).read(20L until 40L).assertSuccess() assertThat(deobfuscatedRes).isEqualTo(font.copyOfRange(20, 40)) } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/search/SearchPagingSource.kt b/test-app/src/main/java/org/readium/r2/testapp/search/SearchPagingSource.kt index f6082263de..06227a355a 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/search/SearchPagingSource.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/search/SearchPagingSource.kt @@ -12,7 +12,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.LocatorCollection import org.readium.r2.shared.publication.services.search.SearchTry -import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.assertSuccess @OptIn(ExperimentalReadiumApi::class) class SearchPagingSource( @@ -31,7 +31,7 @@ class SearchPagingSource( listener ?: return LoadResult.Page(data = emptyList(), prevKey = null, nextKey = null) return try { - val page = listener.next().getOrThrow() + val page = listener.next().assertSuccess() LoadResult.Page( data = page?.locators ?: emptyList(), prevKey = null, From dec440a81117725e1ad7e3ff218668425e484519 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Fri, 3 Nov 2023 19:33:02 +0100 Subject: [PATCH 06/86] Refactor ArchiveFactory --- .../adapter/pdfium/document/PdfiumDocument.kt | 11 +- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 2 +- .../org/readium/r2/lcp/LcpDecryptorTest.kt | 2 +- .../r2/shared/publication/Publication.kt | 13 +- .../java/org/readium/r2/shared/util/Error.kt | 12 +- .../r2/shared/util/asset/AssetRetriever.kt | 163 ++++---- .../{ => util}/datasource/DataSource.kt | 2 +- .../datasource/DataSourceDecoder.kt | 30 +- .../datasource/DataSourceInputStream.kt | 20 +- .../r2/shared/util/http/DefaultHttpClient.kt | 3 +- .../util/mediatype/MediaTypeRetriever.kt | 8 +- .../shared/util/mediatype/MediaTypeSniffer.kt | 378 ++++++++++-------- .../util/mediatype/MediaTypeSnifferContent.kt | 73 +++- .../r2/shared/util/resource/ArchiveFactory.kt | 72 ++++ .../shared/util/resource/ArchiveProvider.kt | 11 + .../r2/shared/util/resource/Factories.kt | 135 ------- .../r2/shared/util/resource/FileResource.kt | 10 +- .../util/resource/FileZipArchiveFactory.kt | 70 ---- .../util/resource/FileZipArchiveProvider.kt | 111 +++++ .../r2/shared/util/resource/MediaTypeExt.kt | 29 +- .../r2/shared/util/resource/Resource.kt | 32 ++ .../util/resource/ResourceDataSource.kt | 34 +- .../shared/util/resource/ResourceFactory.kt | 57 +++ .../util/resource/ResourceInputStream.kt | 9 +- .../r2/shared/util/zip/ChannelZipContainer.kt | 13 +- ...esourceChannel.kt => DatasourceChannel.kt} | 30 +- .../util/zip/StreamingZipArchiveFactory.kt | 78 ---- .../util/zip/StreamingZipArchiveProvider.kt | 131 ++++++ .../util/mediatype/MediaTypeRetrieverTest.kt | 4 +- .../shared/util/resource/ZipContainerTest.kt | 6 +- .../streamer/parser/image/ImageParserTest.kt | 4 +- .../java/org/readium/r2/testapp/Readium.kt | 19 +- .../r2/testapp/reader/ReaderViewModel.kt | 6 +- 33 files changed, 899 insertions(+), 679 deletions(-) rename readium/shared/src/main/java/org/readium/r2/shared/{ => util}/datasource/DataSource.kt (95%) rename readium/shared/src/main/java/org/readium/r2/shared/{ => util}/datasource/DataSourceDecoder.kt (81%) rename readium/shared/src/main/java/org/readium/r2/shared/{ => util}/datasource/DataSourceInputStream.kt (87%) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProvider.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveProvider.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/zip/{ResourceChannel.kt => DatasourceChannel.kt} (76%) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt diff --git a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt index bc8ca0c25c..1f12e0ff78 100644 --- a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt +++ b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt @@ -18,6 +18,8 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.extensions.md5 import org.readium.r2.shared.extensions.tryOrNull +import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory @@ -104,11 +106,10 @@ public class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory = use { - read() - .decode( - { core.fromBytes(it, password) }, - { "Pdfium could not read data." } - ) + it.decode( + { bytes -> core.fromBytes(bytes, password) }, + { MessageError("Pdfium could not read data.", ThrowableError(it)) } + ) } private fun PdfiumCore.fromFile(file: File, password: String?): PdfiumDocument = diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 59108bc02c..2e1b01f74d 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -18,8 +18,8 @@ import org.readium.r2.shared.util.MessageError 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.getOrElse import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.FailureResource import org.readium.r2.shared.util.resource.Resource diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt index 79814ae8b9..7fd861c18b 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt @@ -16,9 +16,9 @@ import org.readium.r2.shared.extensions.coerceIn import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.getOrThrow -import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use import timber.log.Timber diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index abf33d33d2..fd05f16aa5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -34,8 +34,8 @@ import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.EmptyContainer import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceError -import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.fallback +import org.readium.r2.shared.util.resource.withMediaType internal typealias ServiceFactory = (Publication.Service.Context) -> Publication.Service? @@ -630,14 +630,3 @@ public class Publication( ) public sealed class OpeningException } - -private fun Resource.withMediaType(mediaType: MediaType?): Resource { - if (mediaType == null) { - return this - } - - return object : Resource by this { - override suspend fun mediaType(): ResourceTry = - ResourceTry.success(mediaType) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt index d259670992..008307dd29 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt @@ -55,14 +55,10 @@ public fun Try.assertSuccess(): S = is Try.Success -> value is Try.Failure -> - throw IllegalStateException("Try was excepted to contain a success.") - } -public fun Try.assertSuccess(): S = - when (this) { - is Try.Success -> - value - is Try.Failure -> - throw IllegalStateException("Try was excepted to contain a success.", value) + throw IllegalStateException( + "Try was excepted to contain a success.", + value as? Throwable + ) } public class FilesystemError( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index b7af677f92..2e8ebbfee5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -24,18 +24,21 @@ import org.readium.r2.shared.util.NetworkError 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.mediatype.CompositeMediaTypeSniffer 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 org.readium.r2.shared.util.mediatype.MediaTypeSniffer import org.readium.r2.shared.util.mediatype.MediaTypeSnifferContentError +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.ArchiveFactory +import org.readium.r2.shared.util.resource.ArchiveProvider +import org.readium.r2.shared.util.resource.CompositeArchiveFactory import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.ContainerMediaTypeSnifferContent import org.readium.r2.shared.util.resource.FileResourceFactory -import org.readium.r2.shared.util.resource.FileZipArchiveFactory +import org.readium.r2.shared.util.resource.FileZipArchiveProvider import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.ResourceFactory @@ -49,9 +52,14 @@ import org.readium.r2.shared.util.toUrl public class AssetRetriever( private val mediaTypeRetriever: MediaTypeRetriever, private val resourceFactory: ResourceFactory, - private val archiveFactory: ArchiveFactory, - private val contentResolver: ContentResolver + private val contentResolver: ContentResolver, + archiveProviders: List = listOf(FileZipArchiveProvider(mediaTypeRetriever)) ) { + private val archiveSniffer: MediaTypeSniffer = + CompositeMediaTypeSniffer(archiveProviders) + + private val archiveFactory: ArchiveFactory = + CompositeArchiveFactory(archiveProviders) public companion object { public operator fun invoke(context: Context): AssetRetriever { @@ -59,7 +67,7 @@ public class AssetRetriever( return AssetRetriever( mediaTypeRetriever = mediaTypeRetriever, resourceFactory = FileResourceFactory(mediaTypeRetriever), - archiveFactory = FileZipArchiveFactory(mediaTypeRetriever), + archiveProviders = emptyList(), contentResolver = context.contentResolver ) } @@ -110,6 +118,7 @@ public class AssetRetriever( public constructor(url: AbsoluteUrl, exception: Exception) : this(url, ThrowableError(exception)) } + public class Network(public override val cause: NetworkError) : Error("A network error occurred.", cause) @@ -137,6 +146,7 @@ public class AssetRetriever( return when (containerType) { null -> retrieveResourceAsset(url, mediaType) + else -> retrieveArchiveAsset(url, mediaType, containerType) } @@ -147,28 +157,40 @@ public class AssetRetriever( mediaType: MediaType, containerType: MediaType ): Try { - return retrieveResource(url, mediaType) - .flatMap { resource: Resource -> - archiveFactory.create(resource, containerType, password = null) - .mapFailure { error -> - when (error) { - is ArchiveFactory.Error.FormatNotSupported -> - Error.ArchiveFormatNotSupported(error) - is ArchiveFactory.Error.ResourceError -> - error.cause.wrap(url) - is ArchiveFactory.Error.PasswordsNotSupported -> - Error.ArchiveFormatNotSupported(error) - } - } - } - .map { container -> - Asset.Container( - mediaType, - containerType, - container.container - ) - } + val resource = retrieveResource(url, containerType) + .getOrElse { return Try.failure(it) } + + return retrieveArchiveAsset(url, resource, mediaType, containerType) } + private suspend fun retrieveArchiveAsset( + url: AbsoluteUrl, + resource: Resource, + mediaType: MediaType, + containerType: MediaType + ): Try { + val container = archiveFactory.create(resource) + .getOrElse { error -> return Try.failure(error.toAssetRetrieverError(url)) } + + val asset = Asset.Container( + mediaType = mediaType, + containerType = containerType, + container = container + ) + + return Try.success(asset) + } + + private fun ArchiveFactory.Error.toAssetRetrieverError(url: AbsoluteUrl): Error = + when (this) { + is ArchiveFactory.Error.UnsupportedFormat -> + Error.ArchiveFormatNotSupported(this) + + is ArchiveFactory.Error.ResourceError -> + cause.wrap(url) + + is ArchiveFactory.Error.PasswordsNotSupported -> + Error.ArchiveFormatNotSupported(this) + } private suspend fun retrieveResourceAsset( url: AbsoluteUrl, @@ -200,16 +222,22 @@ public class AssetRetriever( when (this) { is ResourceError.Forbidden -> Error.Forbidden(url, this) + is ResourceError.NotFound -> Error.InvalidAsset(this) + is ResourceError.Network -> Error.Network(cause) + is ResourceError.OutOfMemory -> Error.OutOfMemory(cause.throwable) + is ResourceError.Other -> Error.Unknown(this) + is ResourceError.InvalidContent -> Error.InvalidAsset(this) + is ResourceError.Filesystem -> Error.Filesystem(cause) } @@ -236,31 +264,27 @@ public class AssetRetriever( ) } - return when ( - val archive = archiveFactory.create( - resource, - archiveType = null, - password = null - ) - ) { - is Try.Failure -> - when (archive.value) { - is ArchiveFactory.Error.PasswordsNotSupported -> - return Try.failure(Error.ArchiveFormatNotSupported(archive.value)) - is ArchiveFactory.Error.ResourceError -> - return Try.failure(archive.value.cause.wrap(url)) - is ArchiveFactory.Error.FormatNotSupported -> - retrieve(url, resource) - .mapFailure {it.wrap(url) } + val mediaType = retrieveMediaType(url, Either.Left(resource)) + .getOrElse { return Try.failure(it.wrap(url)) } + + return archiveSniffer.sniffResource(ResourceMediaTypeSnifferContent(resource)) + .fold( + { containerType -> + retrieveArchiveAsset(url, mediaType = mediaType, containerType = containerType) + }, + { error -> + when (error) { + MediaTypeSnifferError.NotRecognized -> + Try.success(Asset.Resource(mediaType, resource)) + is MediaTypeSnifferError.SourceError -> + Try.failure(error.wrap(url)) + } } - is Try.Success -> - retrieve(url, archive.value.container, archive.value.mediaType) - .mapFailure { it.wrap(url) } - } + ) } - private fun MediaTypeSniffer.Error.wrap(url: AbsoluteUrl) = when (this) { - is MediaTypeSniffer.Error.SourceError -> + private fun MediaTypeSnifferError.wrap(url: AbsoluteUrl) = when (this) { + is MediaTypeSnifferError.SourceError -> when (cause) { is MediaTypeSnifferContentError.Filesystem -> Error.Filesystem(cause.cause) @@ -270,43 +294,22 @@ public class AssetRetriever( Error.Network(cause.cause) is MediaTypeSnifferContentError.NotFound -> Error.NotFound(url, cause.cause) + is MediaTypeSnifferContentError.ArchiveError -> + Error.InvalidAsset(cause) + is MediaTypeSnifferContentError.TooBig -> + Error.OutOfMemory(cause.cause.throwable) + is MediaTypeSnifferContentError.Unknown -> + Error.Unknown(cause) } - MediaTypeSniffer.Error.NotRecognized -> + MediaTypeSnifferError.NotRecognized -> Error.Unknown(MessageError("Cannot determine media type.")) } - private suspend fun retrieve( - url: AbsoluteUrl, - container: Container, - containerType: MediaType - ): Try { - val mediaType = retrieveMediaType(url, Either(container)) - .getOrElse { return Try.failure(it) } - - val asset = - Asset.Container( - mediaType, - containerType = containerType, - container = container - ) - - return Try.success(asset) - } - - private suspend fun retrieve(url: AbsoluteUrl, resource: Resource): Try { - val mediaType = retrieveMediaType(url, Either(resource)) - .getOrElse { return Try.failure(it) } - - val asset = Asset.Resource(mediaType, resource = resource) - - return Try.success(asset) - } - private suspend fun retrieveMediaType( url: AbsoluteUrl, asset: Either - ): Try { - suspend fun retrieve(hints: MediaTypeHints): Try = + ): Try { + suspend fun retrieve(hints: MediaTypeHints): Try = mediaTypeRetriever.retrieve( hints = hints, content = when (asset) { @@ -318,7 +321,7 @@ public class AssetRetriever( retrieve(MediaTypeHints(fileExtensions = listOfNotNull(url.extension))) .onSuccess { return Try.success(it) } .onFailure { error -> - if (error is MediaTypeSniffer.Error.SourceError) { + if (error is MediaTypeSnifferError.SourceError) { return Try.failure(error) } } @@ -343,6 +346,6 @@ public class AssetRetriever( ?.let { return Try.success(it) } } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSource.kt similarity index 95% rename from readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSource.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSource.kt index 9c5765fdba..b8c838f946 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSource.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.datasource +package org.readium.r2.shared.util.datasource import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Try diff --git a/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceDecoder.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSourceDecoder.kt similarity index 81% rename from readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceDecoder.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSourceDecoder.kt index 2396e99932..ae98b78a3b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceDecoder.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSourceDecoder.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.datasource +package org.readium.r2.shared.util.datasource import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -22,21 +22,20 @@ import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.xml.ElementNode import org.readium.r2.shared.util.xml.XmlParser -internal sealed class DecoderError( +internal sealed class DecoderError( override val message: String -): Error { +) : Error { - class DataSourceError( + class DataSourceError( override val cause: E ) : DecoderError("Data source error") - class DecodingError( + class DecodingError( override val cause: Error? ) : DecoderError("Decoding Error") - } -internal suspend fun Try.decode( +internal suspend fun Try.decode( block: (value: S) -> R, wrapException: (Exception) -> Error ): Try> = @@ -55,7 +54,7 @@ internal suspend fun Try.decode( Try.failure(DecoderError.DataSourceError(value)) } -internal suspend fun Try>.decode( +internal suspend fun Try>.decodeMap( block: (value: S) -> R, wrapException: (Exception) -> Error ): Try> = @@ -73,22 +72,23 @@ internal suspend fun Try>.decode( is Try.Failure -> Try.failure(value) } + /** * Content as plain text. * * It will extract the charset parameter from the media type hints to figure out an encoding. * Otherwise, fallback on UTF-8. */ -internal suspend fun DataSource.readAsString( +internal suspend fun DataSource.readAsString( charset: Charset = Charsets.UTF_8 ): Try> = read().decode( { String(it, charset = charset) }, - { MessageError("Content is not a valid $charset string.", ThrowableError(it)) } + { MessageError("Content is not a valid $charset string.", ThrowableError(it)) } ) /** Content as an XML document. */ -internal suspend fun DataSource.readAsXml(): Try> = +internal suspend fun DataSource.readAsXml(): Try> = read().decode( { XmlParser().parse(ByteArrayInputStream(it)) }, { MessageError("Content is not a valid XML document.", ThrowableError(it)) } @@ -97,14 +97,14 @@ internal suspend fun DataSource.readAsXml(): Try DataSource.readAsJson(): Try> = - readAsString().decode( +internal suspend fun DataSource.readAsJson(): Try> = + readAsString().decodeMap( { JSONObject(it) }, { MessageError("Content is not valid JSON.", ThrowableError(it)) } ) /** Readium Web Publication Manifest parsed from the content. */ -internal suspend fun DataSource.readAsRwpm(): Try> = +internal suspend fun DataSource.readAsRwpm(): Try> = readAsJson().flatMap { json -> Manifest.fromJSON(json) ?.let { Try.success(it) } @@ -118,7 +118,7 @@ internal suspend fun DataSource.readAsRwpm(): Try DataSource.readAsBitmap(): Try> = +internal suspend fun DataSource.readAsBitmap(): Try> = read() .mapFailure { DecoderError.DataSourceError(it) } .flatMap { bytes -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSourceInputStream.kt similarity index 87% rename from readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceInputStream.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSourceInputStream.kt index ae3d1160c9..e159bd9d06 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/datasource/DataSourceInputStream.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSourceInputStream.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.datasource +package org.readium.r2.shared.util.datasource import java.io.IOException import java.io.InputStream @@ -18,7 +18,7 @@ import org.readium.r2.shared.util.getOrThrow * If you experience bad performances, consider wrapping the stream in a BufferedInputStream. This * is particularly useful when streaming deflated ZIP entries. */ -internal class DataSourceInputStream( +internal class DataSourceInputStream( private val dataSource: DataSource, private val wrapError: (E) -> IOException, private val range: LongRange? = null @@ -68,9 +68,11 @@ internal class DataSourceInputStream( return -1 } - val bytes = runBlocking { dataSource.read(position until (position + 1)) - .mapFailure { wrapError(it) } - .getOrThrow() } + val bytes = runBlocking { + dataSource.read(position until (position + 1)) + .mapFailure { wrapError(it) } + .getOrThrow() + } position += 1 return bytes.first().toUByte().toInt() } @@ -83,9 +85,11 @@ internal class DataSourceInputStream( } val bytesToRead = len.coerceAtMost(available()) - val bytes = runBlocking { dataSource.read(position until (position + bytesToRead)) - .mapFailure { wrapError(it) } - .getOrThrow() } + val bytes = runBlocking { + dataSource.read(position until (position + bytesToRead)) + .mapFailure { wrapError(it) } + .getOrThrow() + } check(bytes.size <= bytesToRead) bytes.copyInto( destination = b, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index 21fd81399b..55ad42a978 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -24,6 +24,7 @@ import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.e import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.getOrDefault import org.readium.r2.shared.util.http.HttpRequest.Method import org.readium.r2.shared.util.mediatype.BytesResourceMediaTypeSnifferContent import org.readium.r2.shared.util.mediatype.MediaType @@ -172,7 +173,7 @@ public class DefaultHttpClient( mediaTypeRetriever.retrieve( hints = MediaTypeHints(connection), content = BytesResourceMediaTypeSnifferContent { it } - ) + ).getOrDefault(MediaType.BINARY) } return@withContext Try.failure(HttpError(kind, mediaType, body)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt index 264be556ce..2930c159b5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt @@ -95,7 +95,7 @@ public class MediaTypeRetriever( public suspend fun retrieve( hints: MediaTypeHints = MediaTypeHints(), content: MediaTypeSnifferContent? = null - ): Try { + ): Try { for (sniffer in sniffers) { sniffer.sniffHints(hints) .getOrNull() @@ -107,7 +107,7 @@ public class MediaTypeRetriever( sniffer.sniffContent(content) .onSuccess { return Try.success(it) } .onFailure { error -> - if (error is MediaTypeSniffer.Error.SourceError) { + if (error is MediaTypeSnifferError.SourceError) { return Try.failure(error) } } @@ -126,7 +126,7 @@ public class MediaTypeRetriever( SystemMediaTypeSniffer.sniffContent(content) .onSuccess { return Try.success(it) } .onFailure { error -> - if (error is MediaTypeSniffer.Error.SourceError) { + if (error is MediaTypeSnifferError.SourceError) { return Try.failure(error) } } @@ -134,6 +134,6 @@ public class MediaTypeRetriever( return hints.mediaTypes.firstOrNull() ?.let { Try.success(it) } - ?: Try.failure(MediaTypeSniffer.Error.NotRecognized) + ?: Try.failure(MediaTypeSnifferError.NotRecognized) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index f4883477eb..d2e350ada1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -12,43 +12,133 @@ import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject -import org.readium.r2.shared.datasource.DecoderError import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.Error as BaseError import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.datasource.DecoderError import org.readium.r2.shared.util.getOrElse +public sealed class MediaTypeSnifferError( + override val message: String, + override val cause: BaseError? +) : BaseError { + public data object NotRecognized : + MediaTypeSnifferError("Media type of resource could not be inferred.", null) + + public data class SourceError(override val cause: MediaTypeSnifferContentError) : + MediaTypeSnifferError("An error occurred while trying to read content.", cause) +} + +public interface HintMediaTypeSniffer { + public fun sniffHints( + hints: MediaTypeHints + ): Try +} + +public interface ResourceMediaTypeSniffer { + public suspend fun sniffResource( + resource: ResourceMediaTypeSnifferContent + ): Try +} + +public interface ContainerMediaTypeSniffer { + public suspend fun sniffContainer( + container: ContainerMediaTypeSnifferContent + ): Try +} + /** * Sniffs a [MediaType] from media type and file extension hints or asset content. */ -public interface MediaTypeSniffer { - - public sealed class Error( - override val message: String, - override val cause: org.readium.r2.shared.util.Error? - ) : org.readium.r2.shared.util.Error { - public data object NotRecognized : - Error("Media type of resource could not be inferred.", null) - - public data class SourceError(override val cause: MediaTypeSnifferContentError) : - Error("An error occurred while trying to read content.", cause) - } +public interface MediaTypeSniffer : HintMediaTypeSniffer, ResourceMediaTypeSniffer, ContainerMediaTypeSniffer { /** * Sniffs a [MediaType] from media type and file extension hints. */ - public fun sniffHints(hints: MediaTypeHints): Try = - Try.failure(Error.NotRecognized) + public override fun sniffHints( + hints: MediaTypeHints + ): Try = + Try.failure(MediaTypeSnifferError.NotRecognized) /** - * Sniffs a [MediaType] from an asset [content]. + * Sniffs a [MediaType] from a [ResourceMediaTypeSnifferContent]. */ - public suspend fun sniffContent(content: MediaTypeSnifferContent): Try = - Try.failure(Error.NotRecognized) + public override suspend fun sniffResource( + resource: ResourceMediaTypeSnifferContent + ): Try = + Try.failure(MediaTypeSnifferError.NotRecognized) + + /** + * Sniffs a [MediaType] from a [ContainerMediaTypeSnifferContent]. + */ + public override suspend fun sniffContainer( + container: ContainerMediaTypeSnifferContent + ): Try = + Try.failure(MediaTypeSnifferError.NotRecognized) +} + +internal suspend fun MediaTypeSniffer.sniffContent( + content: MediaTypeSnifferContent +): Try = + when (content) { + is ContainerMediaTypeSnifferContent -> + sniffContainer(content) + is ResourceMediaTypeSnifferContent -> + sniffResource(content) + } + +internal class CompositeMediaTypeSniffer( + private val sniffers: List +) : MediaTypeSniffer { + + override fun sniffHints(hints: MediaTypeHints): Try { + for (sniffer in sniffers) { + sniffer.sniffHints(hints) + .getOrNull() + ?.let { return Try.success(it) } + } + + return Try.failure(MediaTypeSnifferError.NotRecognized) + } + + override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { + for (sniffer in sniffers) { + sniffer.sniffResource(resource) + .getOrElse { error -> + when (error) { + MediaTypeSnifferError.NotRecognized -> + null + is MediaTypeSnifferError.SourceError -> + return Try.failure(error) + } + } + ?.let { return Try.success(it) } + } + + return Try.failure(MediaTypeSnifferError.NotRecognized) + } + + override suspend fun sniffContainer(container: ContainerMediaTypeSnifferContent): Try { + for (sniffer in sniffers) { + sniffer.sniffContainer(container) + .getOrElse { error -> + when (error) { + MediaTypeSnifferError.NotRecognized -> + null + is MediaTypeSnifferError.SourceError -> + return Try.failure(error) + } + } + ?.let { return Try.success(it) } + } + + return Try.failure(MediaTypeSnifferError.NotRecognized) + } } /** @@ -57,7 +147,7 @@ public interface MediaTypeSniffer { * Must precede the HTML sniffer. */ public object XhtmlMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("xht", "xhtml") || hints.hasMediaType("application/xhtml+xml") @@ -65,19 +155,15 @@ public object XhtmlMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.XHTML) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { - if (content !is ResourceMediaTypeSnifferContent) { - return Try.failure(MediaTypeSniffer.Error.NotRecognized) - } - - content.contentAsXml() + override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { + resource.contentAsXml() .getOrElse { when (it) { is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) is DecoderError.DecodingError -> null } @@ -89,13 +175,13 @@ public object XhtmlMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.XHTML) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } /** Sniffs an HTML document. */ public object HtmlMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("htm", "html") || hints.hasMediaType("text/html") @@ -103,32 +189,28 @@ public object HtmlMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.HTML) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { - if (content !is ResourceMediaTypeSnifferContent) { - return Try.failure(MediaTypeSniffer.Error.NotRecognized) - } - + override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { // [contentAsXml] will fail if the HTML is not a proper XML document, hence the doctype check. - content.contentAsXml() + resource.contentAsXml() .getOrElse { when (it) { is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) is DecoderError.DecodingError -> null } } - ?.takeIf { it.name.lowercase(Locale.ROOT) == "html" } + ?.takeIf { it.name.lowercase(Locale.ROOT) == "html" } ?.let { return Try.success(MediaType.HTML) } - content.contentAsString() + resource.contentAsString() .getOrElse { when (it) { is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) is DecoderError.DecodingError -> null @@ -137,14 +219,14 @@ public object HtmlMediaTypeSniffer : MediaTypeSniffer { ?.takeIf { it.trimStart().take(15).lowercase() == "" } ?.let { return Try.success(MediaType.HTML) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } /** Sniffs an OPDS document. */ public object OpdsMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { + override fun sniffHints(hints: MediaTypeHints): Try { // OPDS 1 if (hints.hasMediaType("application/atom+xml;type=entry;profile=opds-catalog")) { return Try.success(MediaType.OPDS1_ENTRY) @@ -175,20 +257,16 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.OPDS_AUTHENTICATION) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { - if (content !is ResourceMediaTypeSnifferContent) { - return Try.failure(MediaTypeSniffer.Error.NotRecognized) - } - + override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { // OPDS 1 - content.contentAsXml() + resource.contentAsXml() .getOrElse { when (it) { is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) is DecoderError.DecodingError -> null } @@ -203,11 +281,11 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { } // OPDS 2 - content.contentAsRwpm() + resource.contentAsRwpm() .getOrElse { when (it) { is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) is DecoderError.DecodingError -> null } @@ -224,32 +302,37 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { fun List.firstWithRelMatching(predicate: (String) -> Boolean): Link? = firstOrNull { it.rels.any(predicate) } - if (rwpm.links.firstWithRelMatching { it.startsWith("http://opds-spec.org/acquisition") } != null) { + if (rwpm.links.firstWithRelMatching { + it.startsWith( + "http://opds-spec.org/acquisition" + ) + } != null + ) { return Try.success(MediaType.OPDS2_PUBLICATION) } } // OPDS Authentication Document. - content.containsJsonKeys("id", "title", "authentication") + resource.containsJsonKeys("id", "title", "authentication") .getOrElse { when (it) { is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) is DecoderError.DecodingError -> null } } ?.takeIf { it } - ?.let { return Try.success(MediaType.OPDS_AUTHENTICATION) } + ?.let { return Try.success(MediaType.OPDS_AUTHENTICATION) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } /** Sniffs an LCP License Document. */ public object LcpLicenseMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("lcpl") || hints.hasMediaType("application/vnd.readium.lcp.license.v1.0+json") @@ -257,18 +340,15 @@ public object LcpLicenseMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.LCP_LICENSE_DOCUMENT) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { - if (content !is ResourceMediaTypeSnifferContent) { - return Try.failure(MediaTypeSniffer.Error.NotRecognized) - } - content.containsJsonKeys("id", "issued", "provider", "encryption") + override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { + resource.containsJsonKeys("id", "issued", "provider", "encryption") .getOrElse { when (it) { is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) is DecoderError.DecodingError -> null @@ -277,13 +357,13 @@ public object LcpLicenseMediaTypeSniffer : MediaTypeSniffer { ?.takeIf { it } ?.let { return Try.success(MediaType.LCP_LICENSE_DOCUMENT) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } /** Sniffs a bitmap image. */ public object BitmapMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("avif") || hints.hasMediaType("image/avif") @@ -332,13 +412,13 @@ public object BitmapMediaTypeSniffer : MediaTypeSniffer { ) { return Try.success(MediaType.WEBP) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } /** Sniffs a Readium Web Manifest. */ public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { + override fun sniffHints(hints: MediaTypeHints): Try { if (hints.hasMediaType("application/audiobook+json")) { return Try.success(MediaType.READIUM_AUDIOBOOK_MANIFEST) } @@ -351,26 +431,22 @@ public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.READIUM_WEBPUB_MANIFEST) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { - if (content !is ResourceMediaTypeSnifferContent) { - return Try.failure(MediaTypeSniffer.Error.NotRecognized) - } - + override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { val manifest: Manifest = - content.contentAsRwpm() + resource.contentAsRwpm() .getOrElse { when (it) { is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) is DecoderError.DecodingError -> null } } - ?: return Try.failure(MediaTypeSniffer.Error.NotRecognized) + ?: return Try.failure(MediaTypeSnifferError.NotRecognized) if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { return Try.success(MediaType.READIUM_AUDIOBOOK_MANIFEST) @@ -383,13 +459,13 @@ public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.READIUM_WEBPUB_MANIFEST) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } /** Sniffs a Readium Web Publication, protected or not by LCP. */ public object WebPubMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("audiobook") || hints.hasMediaType("application/audiobook+zip") @@ -424,29 +500,25 @@ public object WebPubMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.LCP_PROTECTED_PDF) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { - if (content !is ContainerMediaTypeSnifferContent) { - return Try.failure(MediaTypeSniffer.Error.NotRecognized) - } - + override suspend fun sniffContainer(container: ContainerMediaTypeSnifferContent): Try { // Reads a RWPM from a manifest.json archive entry. val manifest: Manifest = - content.read(RelativeUrl("manifest.json")!!) + container.read(RelativeUrl("manifest.json")!!) .getOrElse { error -> when (error) { is MediaTypeSnifferContentError.NotFound -> null else -> - return Try.failure(MediaTypeSniffer.Error.SourceError(error)) + return Try.failure(MediaTypeSnifferError.SourceError(error)) } } ?.let { tryOrNull { Manifest.fromJSON(JSONObject(String(it))) } } - ?: return Try.failure(MediaTypeSniffer.Error.NotRecognized) + ?: return Try.failure(MediaTypeSnifferError.NotRecognized) - val isLcpProtected = content.checkContains(RelativeUrl("license.lcpl")!!) + val isLcpProtected = container.checkContains(RelativeUrl("license.lcpl")!!) .fold( { true }, { error -> @@ -454,7 +526,7 @@ public object WebPubMediaTypeSniffer : MediaTypeSniffer { is MediaTypeSnifferContentError.NotFound -> false else -> - return Try.failure(MediaTypeSniffer.Error.SourceError(error)) + return Try.failure(MediaTypeSnifferError.SourceError(error)) } } ) @@ -476,23 +548,19 @@ public object WebPubMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.READIUM_WEBPUB) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } /** Sniffs a W3C Web Publication Manifest. */ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { - override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { - if (content !is ResourceMediaTypeSnifferContent) { - return Try.failure(MediaTypeSniffer.Error.NotRecognized) - } - + override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. - val string = content.contentAsString() + val string = resource.contentAsString() .getOrElse { when (it) { is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) is DecoderError.DecodingError -> null @@ -505,7 +573,7 @@ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.W3C_WPUB_MANIFEST) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } @@ -515,7 +583,7 @@ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { * Reference: https://www.w3.org/publishing/epub3/epub-ocf.html#sec-zip-container-mime */ public object EpubMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("epub") || hints.hasMediaType("application/epub+zip") @@ -523,21 +591,17 @@ public object EpubMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.EPUB) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { - if (content !is ContainerMediaTypeSnifferContent) { - return Try.failure(MediaTypeSniffer.Error.NotRecognized) - } - - val mimetype = content.read(RelativeUrl("mimetype")!!) + override suspend fun sniffContainer(container: ContainerMediaTypeSnifferContent): Try { + val mimetype = container.read(RelativeUrl("mimetype")!!) .getOrElse { error -> when (error) { is MediaTypeSnifferContentError.NotFound -> null else -> - return Try.failure(MediaTypeSniffer.Error.SourceError(error)) + return Try.failure(MediaTypeSnifferError.SourceError(error)) } } ?.let { String(it, charset = Charsets.US_ASCII).trim() } @@ -545,7 +609,7 @@ public object EpubMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.EPUB) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } @@ -557,7 +621,7 @@ public object EpubMediaTypeSniffer : MediaTypeSniffer { * - https://www.w3.org/TR/pub-manifest/ */ public object LpfMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("lpf") || hints.hasMediaType("application/lpf+zip") @@ -565,34 +629,30 @@ public object LpfMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.LPF) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { - if (content !is ContainerMediaTypeSnifferContent) { - return Try.failure(MediaTypeSniffer.Error.NotRecognized) - } - - content.checkContains(RelativeUrl("index.html")!!) - .onSuccess { return Try.success(MediaType.LPF) } + override suspend fun sniffContainer(container: ContainerMediaTypeSnifferContent): Try { + container.checkContains(RelativeUrl("index.html")!!) + .onSuccess { return Try.success(MediaType.LPF) } .onFailure { error -> when (error) { is MediaTypeSnifferContentError.NotFound -> {} - else -> return Try.failure(MediaTypeSniffer.Error.SourceError(error)) + else -> return Try.failure(MediaTypeSnifferError.SourceError(error)) } } // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. - content.read(RelativeUrl("publication.json")!!) + container.read(RelativeUrl("publication.json")!!) .getOrElse { error -> when (error) { is MediaTypeSnifferContentError.NotFound -> null else -> - return Try.failure(MediaTypeSniffer.Error.SourceError(error)) + return Try.failure(MediaTypeSnifferError.SourceError(error)) } } - ?.let { tryOrNull { String(it) } } + ?.let { tryOrNull { String(it) } } ?.let { manifest -> if ( manifest.contains("@context") && @@ -602,7 +662,7 @@ public object LpfMediaTypeSniffer : MediaTypeSniffer { } } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } @@ -656,7 +716,7 @@ public object ArchiveMediaTypeSniffer : MediaTypeSniffer { "zpl" ) - override fun sniffHints(hints: MediaTypeHints): Try { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("cbz") || hints.hasMediaType( @@ -671,20 +731,20 @@ public object ArchiveMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.ZAB) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { - if (content !is ContainerMediaTypeSnifferContent) { - return Try.failure(MediaTypeSniffer.Error.NotRecognized) - } - + override suspend fun sniffContainer(container: ContainerMediaTypeSnifferContent): Try { fun isIgnored(url: Url): Boolean = url.filename?.startsWith(".") == true || url.filename == "Thumbs.db" suspend fun archiveContainsOnlyExtensions(fileExtensions: List): Boolean = - content.entries()?.all { url -> - isIgnored(url) || url.extension?.let { fileExtensions.contains(it.lowercase(Locale.ROOT)) } == true + container.entries()?.all { url -> + isIgnored(url) || url.extension?.let { + fileExtensions.contains( + it.lowercase(Locale.ROOT) + ) + } == true } ?: false if (archiveContainsOnlyExtensions(cbzExtensions)) { @@ -694,7 +754,7 @@ public object ArchiveMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.ZAB) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } @@ -704,7 +764,7 @@ public object ArchiveMediaTypeSniffer : MediaTypeSniffer { * Reference: https://www.loc.gov/preservation/digital/formats/fdd/fdd000123.shtml */ public object PdfMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { + override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("pdf") || hints.hasMediaType("application/pdf") @@ -712,46 +772,38 @@ public object PdfMediaTypeSniffer : MediaTypeSniffer { return Try.success(MediaType.PDF) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { - if (content !is ResourceMediaTypeSnifferContent) { - return Try.failure(MediaTypeSniffer.Error.NotRecognized) - } - - content.read(0L until 5L) + override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { + resource.read(0L until 5L) .getOrElse { error -> - return Try.failure(MediaTypeSniffer.Error.SourceError(error)) + return Try.failure(MediaTypeSnifferError.SourceError(error)) } .let { tryOrNull { it.toString(Charsets.UTF_8) } } .takeIf { it == "%PDF-" } ?.let { return Try.success(MediaType.PDF) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } /** Sniffs a JSON document. */ public object JsonMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { + override fun sniffHints(hints: MediaTypeHints): Try { if (hints.hasMediaType("application/problem+json")) { return Try.success(MediaType.JSON_PROBLEM_DETAILS) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { - if (content !is ResourceMediaTypeSnifferContent) { - return Try.failure(MediaTypeSniffer.Error.NotRecognized) - } - - content.contentAsJson() + override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { + resource.contentAsJson() .getOrElse { when (it) { is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSniffer.Error.SourceError(it.cause)) + return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) is DecoderError.DecodingError -> null @@ -759,7 +811,7 @@ public object JsonMediaTypeSniffer : MediaTypeSniffer { } ?.let { return Try.success(MediaType.JSON) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } } @@ -771,7 +823,7 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { private val mimetypes = tryOrNull { MimeTypeMap.getSingleton() } - override fun sniffHints(hints: MediaTypeHints): Try { + override fun sniffHints(hints: MediaTypeHints): Try { for (mediaType in hints.mediaTypes) { sniffType(mediaType.toString()) ?.let { return Try.success(it) } @@ -782,15 +834,11 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { ?.let { return Try.success(it) } } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContent(content: MediaTypeSnifferContent): Try { - if (content !is ResourceMediaTypeSnifferContent) { - return Try.failure(MediaTypeSniffer.Error.NotRecognized) - } - - content.contentAsStream() + override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { + resource.contentAsStream() .use { try { withContext(Dispatchers.IO) { @@ -798,12 +846,12 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { ?.let { sniffType(it) } } } catch (e: MediaTypeSnifferContentException) { - return Try.failure(MediaTypeSniffer.Error.SourceError(e.error)) + return Try.failure(MediaTypeSnifferError.SourceError(e.error)) } } ?.let { return Try.success(it) } - return Try.failure(MediaTypeSniffer.Error.NotRecognized) + return Try.failure(MediaTypeSnifferError.NotRecognized) } private fun sniffType(type: String): MediaType? { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt index de8ec47781..4e95cbc294 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt @@ -3,21 +3,23 @@ package org.readium.r2.shared.util.mediatype import java.io.IOException import java.io.InputStream import org.json.JSONObject -import org.readium.r2.shared.datasource.DataSource -import org.readium.r2.shared.datasource.DataSourceInputStream -import org.readium.r2.shared.datasource.DecoderError -import org.readium.r2.shared.datasource.readAsJson -import org.readium.r2.shared.datasource.readAsRwpm -import org.readium.r2.shared.datasource.readAsString -import org.readium.r2.shared.datasource.readAsXml import org.readium.r2.shared.extensions.read import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.FilesystemError import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.NetworkError +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.datasource.DataSource +import org.readium.r2.shared.util.datasource.DataSourceInputStream +import org.readium.r2.shared.util.datasource.DecoderError +import org.readium.r2.shared.util.datasource.readAsJson +import org.readium.r2.shared.util.datasource.readAsRwpm +import org.readium.r2.shared.util.datasource.readAsString +import org.readium.r2.shared.util.datasource.readAsXml import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.xml.ElementNode @@ -31,6 +33,8 @@ public sealed interface MediaTypeSnifferContent */ public interface ResourceMediaTypeSnifferContent : MediaTypeSnifferContent { + public val source: AbsoluteUrl? + /** * Reads all the bytes or the given [range]. * @@ -67,25 +71,21 @@ internal class ResourceMediaTypeSnifferContentDataSource( * It will extract the charset parameter from the media type hints to figure out an encoding. * Otherwise, fallback on UTF-8. */ -internal suspend fun ResourceMediaTypeSnifferContent.contentAsString() -: Try> = +internal suspend fun ResourceMediaTypeSnifferContent.contentAsString(): Try> = asDataSource().readAsString() /** Content as an XML document. */ -internal suspend fun ResourceMediaTypeSnifferContent.contentAsXml() -: Try> = +internal suspend fun ResourceMediaTypeSnifferContent.contentAsXml(): Try> = asDataSource().readAsXml() /** * Content parsed from JSON. */ -internal suspend fun ResourceMediaTypeSnifferContent.contentAsJson() -: Try> = +internal suspend fun ResourceMediaTypeSnifferContent.contentAsJson(): Try> = asDataSource().readAsJson() /** Readium Web Publication Manifest parsed from the content. */ -internal suspend fun ResourceMediaTypeSnifferContent.contentAsRwpm() -: Try> = +internal suspend fun ResourceMediaTypeSnifferContent.contentAsRwpm(): Try> = asDataSource().readAsRwpm() public sealed class MediaTypeSnifferContentError(override val message: String) : Error { @@ -101,6 +101,15 @@ public sealed class MediaTypeSnifferContentError(override val message: String) : public class Filesystem(public override val cause: FilesystemError) : MediaTypeSnifferContentError("An unexpected error occurred on filesystem.") + + public class TooBig(public override val cause: ThrowableError) : + MediaTypeSnifferContentError("Sniffing was interrupted because resource is too big.") + + public class ArchiveError(public override val cause: Error) : + MediaTypeSnifferContentError("An error occurred with archive.") + + public class Unknown(public override val cause: Error) : + MediaTypeSnifferContentError("An unknown error occurred.") } /** @@ -114,7 +123,23 @@ internal fun ResourceMediaTypeSnifferContent.contentAsStream(): InputStream = internal class MediaTypeSnifferContentException( val error: MediaTypeSnifferContentError -): IOException() +) : IOException() { + + companion object { + + fun Exception.unwrapMediaTypeSnifferContentException(): Exception { + this.findMediaTypeSnifferContentExceptionCause()?.let { return it } + return this + } + + private fun Throwable.findMediaTypeSnifferContentExceptionCause(): MediaTypeSnifferContentException? = + when { + this is MediaTypeSnifferContentException -> this + cause != null -> cause!!.findMediaTypeSnifferContentExceptionCause() + else -> null + } + } +} /** * Returns whether the content is a JSON object containing all of the given root keys. @@ -150,12 +175,16 @@ public interface ContainerMediaTypeSnifferContent : MediaTypeSnifferContent { internal suspend fun ContainerMediaTypeSnifferContent.checkContains(url: Url): Try = entries()?.contains(url) ?.let { - if (it) Try.success(Unit) - else Try.failure( - MediaTypeSnifferContentError.NotFound( - MessageError("Container entry list doesn't contain $url.") + if (it) { + Try.success(Unit) + } else { + Try.failure( + MediaTypeSnifferContentError.NotFound( + MessageError("Container entry list doesn't contain $url.") + ) ) - ) } + } + } ?: read(url, range = 0L..1L) .map { } @@ -177,6 +206,8 @@ public class BytesResourceMediaTypeSnifferContent( return _bytes } + override val source: AbsoluteUrl? = null + override suspend fun read(range: LongRange?): Try = Try.success(bytes().read(range)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt new file mode 100644 index 0000000000..293769b23d --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse + +/** + * A factory to create [Container]s from archive [Resource]s. + * + */ +public interface ArchiveFactory { + + public sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { + + public class PasswordsNotSupported( + cause: org.readium.r2.shared.util.Error? = null + ) : Error("Password feature is not supported.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + public class UnsupportedFormat( + cause: org.readium.r2.shared.util.Error? = null + ) : Error("Resource is not supported.", cause) + + public class ResourceError( + override val cause: org.readium.r2.shared.util.resource.ResourceError + ) : Error("An error occurred while attempting to read the resource.", cause) + } + + /** + * Creates a new archive [Container] to access the entries of the given archive. + */ + public suspend fun create( + resource: Resource, + password: String? = null + ): Try +} + +public class CompositeArchiveFactory( + private val factories: List +) : ArchiveFactory { + + public constructor(vararg factories: ArchiveFactory) : this(factories.toList()) + + override suspend fun create( + resource: Resource, + password: String? + ): Try { + for (factory in factories) { + factory.create(resource, password) + .getOrElse { error -> + when (error) { + is ArchiveFactory.Error.UnsupportedFormat -> null + else -> return Try.failure(error) + } + } + ?.let { return Try.success(it) } + } + + return Try.failure(ArchiveFactory.Error.UnsupportedFormat()) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProvider.kt new file mode 100644 index 0000000000..b515f23824 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProvider.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.util.mediatype.MediaTypeSniffer + +public interface ArchiveProvider : MediaTypeSniffer, ArchiveFactory diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt deleted file mode 100644 index 0203e05628..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Factories.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.resource - -import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.Error as SharedError -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.mediatype.MediaType -import org.readium.r2.shared.util.tryRecover - -/** - * A factory to read [Resource]s from [Url]s. - */ -public interface ResourceFactory { - - public sealed class Error( - override val message: String, - override val cause: SharedError? - ) : SharedError { - - public class SchemeNotSupported( - public val scheme: Url.Scheme, - cause: SharedError? = null - ) : Error("Url scheme $scheme is not supported.", cause) - } - - public suspend fun create( - url: AbsoluteUrl, - mediaType: MediaType? = null - ): Try -} - -/** - * A factory to create [Container]s from archive [Resource]s. - * - */ -public interface ArchiveFactory { - - public sealed class Error( - override val message: String, - override val cause: SharedError? - ) : SharedError { - - public class FormatNotSupported( - cause: SharedError? = null - ) : Error("Archive format not supported.", cause) { - - public constructor(exception: Exception) : this(ThrowableError(exception)) - } - - public class PasswordsNotSupported( - cause: SharedError? = null - ) : Error("Password feature is not supported.", cause) { - - public constructor(exception: Exception) : this(ThrowableError(exception)) - } - - public class ResourceError( - override val cause: org.readium.r2.shared.util.resource.ResourceError - ) : Error("An error occurred while attempting to read the resource.", cause) - } - - public data class Result( - val mediaType: MediaType, - val container: Container - ) - - /** - * Creates a new archive [Container] to access the entries of the given archive. - */ - public suspend fun create( - resource: Resource, - archiveType: MediaType? = null, - password: String? = null - ): Try -} - -/** - * A composite archive factory which first tries [primaryFactory] - * and falls back on [fallbackFactory] if it doesn't support the resource. - */ -public class CompositeArchiveFactory( - private val primaryFactory: ArchiveFactory, - private val fallbackFactory: ArchiveFactory -) : ArchiveFactory { - - override suspend fun create( - resource: Resource, - archiveType: MediaType?, - password: String? - ): Try { - return primaryFactory.create(resource, archiveType, password) - .tryRecover { error -> - if ( - error is ArchiveFactory.Error.FormatNotSupported || - archiveType == null && error is ArchiveFactory.Error.ResourceError && - error.cause is ResourceError.InvalidContent - ) { - fallbackFactory.create(resource, archiveType) - } else { - Try.failure(error) - } - } - } -} - -/** - * A composite resource factory which first tries [primaryFactory] - * and falls back on [fallbackFactory] if it doesn't support the scheme. - */ -public class CompositeResourceFactory( - private val primaryFactory: ResourceFactory, - private val fallbackFactory: ResourceFactory -) : ResourceFactory { - - override suspend fun create( - url: AbsoluteUrl, - mediaType: MediaType? - ): Try { - return primaryFactory.create(url, mediaType) - .tryRecover { error -> - if (error is ResourceFactory.Error.SchemeNotSupported) { - fallbackFactory.create(url, mediaType) - } else { - Try.failure(error) - } - } - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt index 6141357167..f669b83373 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt @@ -32,11 +32,11 @@ public class FileResource private constructor( private val mediaTypeRetriever: MediaTypeRetriever? ) : Resource { - public constructor(file: File, mediaType: MediaType) - : this(file, mediaType, null) + public constructor(file: File, mediaType: MediaType) : + this(file, mediaType, null) - public constructor(file: File, mediaTypeRetriever: MediaTypeRetriever) - : this(file, null, mediaTypeRetriever) + public constructor(file: File, mediaTypeRetriever: MediaTypeRetriever) : + this(file, null, mediaTypeRetriever) private val randomAccessFile by lazy { try { @@ -53,7 +53,7 @@ public class FileResource private constructor( override suspend fun mediaType(): ResourceTry = mediaType - ?.let { Try.success(it) } + ?.let { Try.success(it) } ?: mediaTypeRetriever!!.retrieve( hints = MediaTypeHints(fileExtension = file.extension), content = ResourceMediaTypeSnifferContent(this) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt deleted file mode 100644 index d5afce14bb..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveFactory.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.resource - -import java.io.File -import java.io.IOException -import java.util.zip.ZipException -import java.util.zip.ZipFile -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.readium.r2.shared.util.MessageError -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever - -/** - * An [ArchiveFactory] to open local ZIP files with Java's [ZipFile]. - */ -public class FileZipArchiveFactory( - private val mediaTypeRetriever: MediaTypeRetriever -) : ArchiveFactory { - - override suspend fun create( - resource: Resource, - archiveType: MediaType?, - password: String? - ): Try { - if (archiveType != null && !archiveType.matches(MediaType.ZIP)) { - return Try.failure(ArchiveFactory.Error.FormatNotSupported()) - } - - if (password != null) { - return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) - } - - val file = resource.source?.toFile() - ?: return Try.Failure( - ArchiveFactory.Error.FormatNotSupported( - MessageError("Resource not supported because file cannot be directly accessed.") - ) - ) - - val container = open(file) - .getOrElse { return Try.failure(it) } - - return Try.success(ArchiveFactory.Result(MediaType.ZIP, container)) - } - - // Internal for testing purpose - internal suspend fun open(file: File): Try = - withContext(Dispatchers.IO) { - try { - val archive = JavaZipContainer(ZipFile(file), file, mediaTypeRetriever) - Try.success(archive) - } catch (e: ZipException) { - Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.InvalidContent(e))) - } catch (e: SecurityException) { - Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.Forbidden(e))) - } catch (e: IOException) { - Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.Filesystem(e))) - } catch (e: Exception) { - Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.Other(e))) - } - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveProvider.kt new file mode 100644 index 0000000000..4806317f24 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveProvider.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import java.io.File +import java.io.IOException +import java.util.zip.ZipException +import java.util.zip.ZipFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.readium.r2.shared.util.FilesystemError +import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse +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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferContentError +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.mediatype.ResourceMediaTypeSnifferContent + +/** + * An [ArchiveFactory] to open local ZIP files with Java's [ZipFile]. + */ +public class FileZipArchiveProvider( + private val mediaTypeRetriever: MediaTypeRetriever +) : ArchiveProvider { + + override fun sniffHints(hints: MediaTypeHints): Try { + if (hints.hasMediaType("application/zip") || + hints.hasFileExtension("zip") + ) { + return Try.success(MediaType.ZIP) + } + + return Try.failure(MediaTypeSnifferError.NotRecognized) + } + + override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { + val file = resource.source?.toFile() + ?: return Try.Failure(MediaTypeSnifferError.NotRecognized) + + return withContext(Dispatchers.IO) { + try { + JavaZipContainer(ZipFile(file), file, mediaTypeRetriever) + Try.success(MediaType.ZIP) + } catch (e: ZipException) { + Try.failure(MediaTypeSnifferError.NotRecognized) + } catch (e: SecurityException) { + Try.failure( + MediaTypeSnifferError.SourceError( + MediaTypeSnifferContentError.Forbidden(ThrowableError(e)) + ) + ) + } catch (e: IOException) { + Try.failure( + MediaTypeSnifferError.SourceError( + MediaTypeSnifferContentError.Filesystem(FilesystemError(e)) + ) + ) + } catch (e: Exception) { + Try.failure( + MediaTypeSnifferError.SourceError( + MediaTypeSnifferContentError.Unknown(ThrowableError(e)) + ) + ) + } + } + } + + override suspend fun create( + resource: Resource, + password: String? + ): Try { + if (password != null) { + return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) + } + + val file = resource.source?.toFile() + ?: return Try.Failure( + ArchiveFactory.Error.UnsupportedFormat( + MessageError("Resource not supported because file cannot be directly accessed.") + ) + ) + + val container = open(file) + .getOrElse { return Try.failure(it) } + + return Try.success(container) + } + + // Internal for testing purpose + internal suspend fun open(file: File): Try = + withContext(Dispatchers.IO) { + try { + val archive = JavaZipContainer(ZipFile(file), file, mediaTypeRetriever) + Try.success(archive) + } catch (e: ZipException) { + Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.InvalidContent(e))) + } catch (e: SecurityException) { + Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.Forbidden(e))) + } catch (e: IOException) { + Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.Filesystem(e))) + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeExt.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeExt.kt index 5e504e02c0..0ae0aea102 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeExt.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeExt.kt @@ -6,14 +6,15 @@ package org.readium.r2.shared.util.resource +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.ContainerMediaTypeSnifferContent import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeSniffer import org.readium.r2.shared.util.mediatype.MediaTypeSnifferContentError +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.mediatype.ResourceMediaTypeSnifferContent import org.readium.r2.shared.util.tryRecover @@ -21,6 +22,9 @@ public class ResourceMediaTypeSnifferContent( private val resource: Resource ) : ResourceMediaTypeSnifferContent { + override val source: AbsoluteUrl? = + resource.source + override suspend fun read(range: LongRange?): Try = resource.safeRead(range) .mapFailure { it.toMediaTypeSnifferContentError() } @@ -44,7 +48,6 @@ public class ContainerMediaTypeSnifferContent( override suspend fun length(url: Url): Try = container.get(url).length() .mapFailure { it.toMediaTypeSnifferContentError() } - } private suspend fun Resource.safeRead(range: LongRange?): Try { try { @@ -72,27 +75,30 @@ internal fun ResourceError.toMediaTypeSnifferContentError() = is ResourceError.Forbidden -> MediaTypeSnifferContentError.Forbidden(this) is ResourceError.InvalidContent -> + MediaTypeSnifferContentError.ArchiveError(this) is ResourceError.Network -> MediaTypeSnifferContentError.Network(cause) is ResourceError.NotFound -> MediaTypeSnifferContentError.NotFound(this) is ResourceError.Other -> + MediaTypeSnifferContentError.Unknown(this) is ResourceError.OutOfMemory -> + MediaTypeSnifferContentError.TooBig(cause) } -internal fun Try.toResourceTry(): ResourceTry = +internal fun Try.toResourceTry(): ResourceTry = tryRecover { when (it) { - MediaTypeSniffer.Error.NotRecognized -> + MediaTypeSnifferError.NotRecognized -> Try.success(MediaType.BINARY) - else -> - Try.failure(it) + else -> + Try.failure(it) } }.mapFailure { when (it) { - MediaTypeSniffer.Error.NotRecognized -> + MediaTypeSnifferError.NotRecognized -> throw IllegalStateException() - is MediaTypeSniffer.Error.SourceError -> { + is MediaTypeSnifferError.SourceError -> { when (it.cause) { is MediaTypeSnifferContentError.Filesystem -> ResourceError.Filesystem(it.cause.cause) @@ -102,8 +108,13 @@ internal fun Try.toResourceTry(): ResourceTry ResourceError.Network(it.cause.cause) is MediaTypeSnifferContentError.NotFound -> ResourceError.NotFound(it.cause.cause) + is MediaTypeSnifferContentError.ArchiveError -> + ResourceError.InvalidContent(it) + is MediaTypeSnifferContentError.TooBig -> + ResourceError.OutOfMemory(it.cause.cause) + is MediaTypeSnifferContentError.Unknown -> + ResourceError.Other(it) } } } - } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index e80fb93ccf..d4842efba9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -6,8 +6,10 @@ package org.readium.r2.shared.util.resource +import java.io.IOException import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.FilesystemError import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.NetworkError @@ -173,3 +175,33 @@ public inline fun ResourceTry.flatMapCatching(transform: (value: S) -> Try.failure(ResourceError.OutOfMemory(e)) } } + +internal fun Resource.withMediaType(mediaType: MediaType?): Resource { + if (mediaType == null) { + return this + } + + return object : Resource by this { + override suspend fun mediaType(): ResourceTry = + ResourceTry.success(mediaType) + } +} + +internal class ResourceException( + val error: ResourceError +) : IOException(error.message, ErrorException(error)) { + + companion object { + fun Exception.unwrapResourceException(): Exception { + this.findResourceExceptionCause()?.let { return it } + return this + } + + private fun Throwable.findResourceExceptionCause(): ResourceException? = + when { + this is ResourceException -> this + cause != null -> cause!!.findResourceExceptionCause() + else -> null + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceDataSource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceDataSource.kt index fb71f12c0f..77bef5022e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceDataSource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceDataSource.kt @@ -9,13 +9,15 @@ package org.readium.r2.shared.util.resource import android.graphics.Bitmap import java.nio.charset.Charset import org.json.JSONObject -import org.readium.r2.shared.datasource.DataSource -import org.readium.r2.shared.datasource.DecoderError -import org.readium.r2.shared.datasource.readAsBitmap -import org.readium.r2.shared.datasource.readAsJson -import org.readium.r2.shared.datasource.readAsString -import org.readium.r2.shared.datasource.readAsXml +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.datasource.DataSource +import org.readium.r2.shared.util.datasource.DecoderError +import org.readium.r2.shared.util.datasource.decode +import org.readium.r2.shared.util.datasource.readAsBitmap +import org.readium.r2.shared.util.datasource.readAsJson +import org.readium.r2.shared.util.datasource.readAsString +import org.readium.r2.shared.util.datasource.readAsXml import org.readium.r2.shared.util.xml.ElementNode private fun DecoderError.toResourceError() = @@ -25,25 +27,34 @@ private fun DecoderError.toResourceError() = is DecoderError.DecodingError -> ResourceError.InvalidContent(cause) } + +public suspend fun Resource.decode( + block: (value: ByteArray) -> R, + wrapException: (Exception) -> Error +): ResourceTry = + read() + .decode(block, wrapException) + .mapFailure { it.toResourceError() } + /** * Reads the full content as a [String]. * * If [charset] is null, then it falls back on UTF-8. */ public suspend fun Resource.readAsString(charset: Charset = Charsets.UTF_8): ResourceTry = - asDataSource().readAsString(charset).mapFailure{ it.toResourceError() } + asDataSource().readAsString(charset).mapFailure { it.toResourceError() } /** * Reads the full content as a JSON object. */ public suspend fun Resource.readAsJson(): ResourceTry = - asDataSource().readAsJson().mapFailure{ it.toResourceError() } + asDataSource().readAsJson().mapFailure { it.toResourceError() } /** * Reads the full content as an XML document. */ public suspend fun Resource.readAsXml(): ResourceTry = - asDataSource().readAsXml().mapFailure{ it.toResourceError() } + asDataSource().readAsXml().mapFailure { it.toResourceError() } /** * Reads the full content as a [Bitmap]. @@ -51,10 +62,9 @@ public suspend fun Resource.readAsXml(): ResourceTry = public suspend fun Resource.readAsBitmap(): ResourceTry = asDataSource().readAsBitmap().mapFailure { it.toResourceError() } - internal class ResourceDataSource( private val resource: Resource -): DataSource { +) : DataSource { override suspend fun length(): Try = resource.length() @@ -68,4 +78,4 @@ internal class ResourceDataSource( } internal fun Resource.asDataSource() = - ResourceDataSource(this) \ No newline at end of file + ResourceDataSource(this) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt new file mode 100644 index 0000000000..c542b2c9c6 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import kotlin.String +import kotlin.let +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Error as SharedError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType + +/** + * A factory to read [Resource]s from [Url]s. + */ +public interface ResourceFactory { + + public sealed class Error( + override val message: String, + override val cause: SharedError? + ) : SharedError { + + public class SchemeNotSupported( + public val scheme: Url.Scheme, + cause: SharedError? = null + ) : Error("Url scheme $scheme is not supported.", cause) + } + + public suspend fun create( + url: AbsoluteUrl, + mediaType: MediaType? = null + ): Try +} + +public class CompositeResourceFactory( + private val factories: List +) : ResourceFactory { + + public constructor(vararg factories: ResourceFactory) : this(factories.toList()) + + override suspend fun create( + url: AbsoluteUrl, + mediaType: MediaType? + ): Try { + for (factory in factories) { + factory.create(url, mediaType) + .getOrNull() + ?.let { return Try.success(it) } + } + + return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceInputStream.kt index a36e3550b5..d272def7d0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceInputStream.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceInputStream.kt @@ -7,10 +7,7 @@ package org.readium.r2.shared.util.resource import java.io.FilterInputStream -import java.io.IOException -import org.readium.r2.shared.datasource.DataSourceInputStream -import org.readium.r2.shared.util.ErrorException -import org.readium.r2.shared.util.resource.ResourceInputStream.ResourceException +import org.readium.r2.shared.util.datasource.DataSourceInputStream /** * Input stream reading a [Resource]'s content. @@ -26,8 +23,4 @@ public class ResourceInputStream private constructor( public constructor(resource: Resource, range: LongRange? = null) : this(DataSourceInputStream(resource.asDataSource(), ::ResourceException, range)) - - public class ResourceException( - public val error: ResourceError - ) : IOException(error.message, ErrorException(error)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index 1b601aea78..717cc96f31 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -24,6 +24,8 @@ import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.FailureResource import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.resource.ResourceException +import org.readium.r2.shared.util.resource.ResourceException.Companion.unwrapResourceException import org.readium.r2.shared.util.resource.ResourceMediaTypeSnifferContent import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.archive @@ -88,10 +90,13 @@ internal class ChannelZipContainer( readRange(range) } Try.success(bytes) - } catch (e: ResourceChannel.ResourceException) { - Try.failure(e.error) - } catch (e: Exception) { - Try.failure(ResourceError.InvalidContent(e)) + } catch (exception: Exception) { + when (val e = exception.unwrapResourceException()) { + is ResourceException -> + Try.failure(e.error) + else -> + Try.failure(ResourceError.InvalidContent(e)) + } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ResourceChannel.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/DatasourceChannel.kt similarity index 76% rename from readium/shared/src/main/java/org/readium/r2/shared/util/zip/ResourceChannel.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/zip/DatasourceChannel.kt index 70695188fe..4162fddbd8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ResourceChannel.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/DatasourceChannel.kt @@ -14,22 +14,18 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.datasource.DataSource import org.readium.r2.shared.util.getOrThrow -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.zip.jvm.ClosedChannelException import org.readium.r2.shared.util.zip.jvm.NonWritableChannelException import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel -internal class ResourceChannel( - private val resource: Resource +internal class DatasourceChannel( + private val dataSource: DataSource, + private val wrapError: (E) -> IOException ) : SeekableByteChannel { - class ResourceException( - val error: ResourceError - ) : IOException(error.message) - private val coroutineScope: CoroutineScope = MainScope() @@ -45,7 +41,7 @@ internal class ResourceChannel( } isClosed = true - coroutineScope.launch { resource.close() } + coroutineScope.launch { dataSource.close() } } override fun isOpen(): Boolean { @@ -59,8 +55,9 @@ internal class ResourceChannel( } withContext(Dispatchers.IO) { - val size = resource.length() - .getOrElse { throw ResourceException(it) } + val size = dataSource.length() + .mapFailure(wrapError) + .getOrThrow() if (position >= size) { return@withContext -1 @@ -69,8 +66,9 @@ internal class ResourceChannel( val available = size - position val toBeRead = dst.remaining().coerceAtMost(available.toInt()) check(toBeRead > 0) - val bytes = resource.read(position until position + toBeRead) - .getOrElse { throw ResourceException(it) } + val bytes = dataSource.read(position until position + toBeRead) + .mapFailure(wrapError) + .getOrThrow() check(bytes.size == toBeRead) dst.put(bytes, 0, toBeRead) position += toBeRead @@ -101,8 +99,8 @@ internal class ResourceChannel( throw ClosedChannelException() } - return runBlocking { resource.length() } - .mapFailure { IOException(ResourceException(it)) } + return runBlocking { dataSource.length() } + .mapFailure { wrapError(it) } .getOrThrow() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt deleted file mode 100644 index 5f7304c24c..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveFactory.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.zip - -import java.io.File -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever -import org.readium.r2.shared.util.resource.ArchiveFactory -import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.toUrl -import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile -import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel - -/** - * An [ArchiveFactory] able to open a ZIP archive served through a stream (e.g. HTTP server, - * content URI, etc.). - */ -public class StreamingZipArchiveFactory( - private val mediaTypeRetriever: MediaTypeRetriever -) : ArchiveFactory { - - override suspend fun create( - resource: Resource, - archiveType: MediaType?, - password: String? - ): Try { - if (archiveType != null && !archiveType.matches(MediaType.ZIP)) { - return Try.failure(ArchiveFactory.Error.FormatNotSupported()) - } - - if (password != null) { - return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) - } - - return try { - val resourceChannel = ResourceChannel(resource) - val channel = wrapBaseChannel(resourceChannel) - val zipFile = ZipFile(channel, true) - val channelZip = ChannelZipContainer(zipFile, resource.source, mediaTypeRetriever) - Try.success(ArchiveFactory.Result(MediaType.ZIP, channelZip)) - } catch (e: ResourceChannel.ResourceException) { - Try.failure(ArchiveFactory.Error.ResourceError(e.error)) - } catch (e: Exception) { - Try.failure(ArchiveFactory.Error.FormatNotSupported(e)) - } - } - - internal fun openFile(file: File): Container { - val fileChannel = FileChannelAdapter(file, "r") - val channel = wrapBaseChannel(fileChannel) - return ChannelZipContainer(ZipFile(channel), file.toUrl(), mediaTypeRetriever) - } - - private fun wrapBaseChannel(channel: SeekableByteChannel): SeekableByteChannel { - val size = channel.size() - return if (size < CACHE_ALL_MAX_SIZE) { - CachingReadableChannel(channel, 0) - } else { - val cacheStart = size - CACHED_TAIL_SIZE - val cachingChannel = CachingReadableChannel(channel, cacheStart) - cachingChannel.cache() - BufferedReadableChannel(cachingChannel, DEFAULT_BUFFER_SIZE) - } - } - - public companion object { - - private const val CACHE_ALL_MAX_SIZE = 5242880 - - private const val CACHED_TAIL_SIZE = 65557 - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt new file mode 100644 index 0000000000..38ff67b3d2 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.zip + +import java.io.File +import java.io.IOException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.datasource.DataSource +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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferContentException +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferContentException.Companion.unwrapMediaTypeSnifferContentException +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.mediatype.ResourceMediaTypeSnifferContent +import org.readium.r2.shared.util.mediatype.asDataSource +import org.readium.r2.shared.util.resource.ArchiveFactory +import org.readium.r2.shared.util.resource.ArchiveProvider +import org.readium.r2.shared.util.resource.Container +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.resource.ResourceException +import org.readium.r2.shared.util.resource.ResourceException.Companion.unwrapResourceException +import org.readium.r2.shared.util.resource.asDataSource +import org.readium.r2.shared.util.toUrl +import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile +import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel + +/** + * An [ArchiveFactory] able to open a ZIP archive served through a stream (e.g. HTTP server, + * content URI, etc.). + */ +public class StreamingZipArchiveProvider( + private val mediaTypeRetriever: MediaTypeRetriever +) : ArchiveProvider { + + override fun sniffHints(hints: MediaTypeHints): Try { + if (hints.hasMediaType("application/zip") || + hints.hasFileExtension("zip") + ) { + return Try.success(MediaType.ZIP) + } + + return Try.failure(MediaTypeSnifferError.NotRecognized) + } + + override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { + val datasource = resource.asDataSource() + + return try { + openDataSource(datasource, ::MediaTypeSnifferContentException, null) + Try.success(MediaType.ZIP) + } catch (exception: Exception) { + when (val e = exception.unwrapMediaTypeSnifferContentException()) { + is MediaTypeSnifferContentException -> + Try.failure(MediaTypeSnifferError.SourceError(e.error)) + else -> + Try.failure(MediaTypeSnifferError.NotRecognized) + } + } + } + + override suspend fun create( + resource: Resource, + password: String? + ): Try { + if (password != null) { + return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) + } + + return try { + val container = openDataSource( + resource.asDataSource(), + ::ResourceException, + resource.source + ) + Try.success(container) + } catch (exception: Exception) { + when (val e = exception.unwrapResourceException()) { + is ResourceException -> + Try.failure(ArchiveFactory.Error.ResourceError(e.error)) + else -> + Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.InvalidContent(e))) + } + } + } + + private suspend fun openDataSource( + dataSource: DataSource, + wrapError: (E) -> IOException, + sourceUrl: AbsoluteUrl? + ): Container = withContext(Dispatchers.IO) { + val datasourceChannel = DatasourceChannel(dataSource, wrapError) + val channel = wrapBaseChannel(datasourceChannel) + val zipFile = ZipFile(channel, true) + ChannelZipContainer(zipFile, sourceUrl, mediaTypeRetriever) + } + + internal suspend fun openFile(file: File): Container = withContext(Dispatchers.IO) { + val fileChannel = FileChannelAdapter(file, "r") + val channel = wrapBaseChannel(fileChannel) + ChannelZipContainer(ZipFile(channel), file.toUrl(), mediaTypeRetriever) + } + + private fun wrapBaseChannel(channel: SeekableByteChannel): SeekableByteChannel { + val size = channel.size() + return if (size < CACHE_ALL_MAX_SIZE) { + CachingReadableChannel(channel, 0) + } else { + val cacheStart = size - CACHED_TAIL_SIZE + val cachingChannel = CachingReadableChannel(channel, cacheStart) + cachingChannel.cache() + BufferedReadableChannel(cachingChannel, DEFAULT_BUFFER_SIZE) + } + } + + public companion object { + + private const val CACHE_ALL_MAX_SIZE = 5242880 + + private const val CACHED_TAIL_SIZE = 65557 + } +} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt index d1e1382f4c..4b3113aacf 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt @@ -10,7 +10,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.FileZipArchiveFactory +import org.readium.r2.shared.util.resource.FileZipArchiveProvider import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf @@ -497,7 +497,7 @@ class MediaTypeRetrieverTest { file: File, hints: MediaTypeHints = MediaTypeHints() ): MediaType? { - val archive = assertNotNull(FileZipArchiveFactory(this).open(file).getOrNull()) + val archive = assertNotNull(FileZipArchiveProvider(this).open(file).getOrNull()) return retrieve( hints, diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt index b110efc128..b79b5d3945 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -23,7 +23,7 @@ import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.use -import org.readium.r2.shared.util.zip.StreamingZipArchiveFactory +import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider import org.robolectric.ParameterizedRobolectricTestRunner @RunWith(ParameterizedRobolectricTestRunner::class) @@ -39,7 +39,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { val zipArchive = suspend { assertNotNull( - FileZipArchiveFactory(MediaTypeRetriever()) + FileZipArchiveProvider(MediaTypeRetriever()) .create( FileResource(File(epubZip.path), mediaType = MediaType.EPUB), password = null @@ -49,7 +49,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { } val apacheZipArchive = suspend { - StreamingZipArchiveFactory(MediaTypeRetriever()) + StreamingZipArchiveProvider(MediaTypeRetriever()) .openFile(File(epubZip.path)) } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index 2ffd8914bf..e65fdddf78 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -22,7 +22,7 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.FileResource -import org.readium.r2.shared.util.resource.FileZipArchiveFactory +import org.readium.r2.shared.util.resource.FileZipArchiveProvider import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toUrl import org.readium.r2.streamer.parseBlocking @@ -37,7 +37,7 @@ class ImageParserTest { private val cbzAsset = runBlocking { val file = fileForResource("futuristic_tales.cbz") val resource = FileResource(file, mediaType = MediaType.CBZ) - val archive = FileZipArchiveFactory(MediaTypeRetriever()).create( + val archive = FileZipArchiveProvider(MediaTypeRetriever()).create( resource, password = null ).getOrNull()!! diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 95d4a05071..19d44ad6b2 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -22,12 +22,10 @@ import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpResourceFactory import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaTypeRetriever -import org.readium.r2.shared.util.resource.CompositeArchiveFactory import org.readium.r2.shared.util.resource.CompositeResourceFactory import org.readium.r2.shared.util.resource.ContentResourceFactory import org.readium.r2.shared.util.resource.FileResourceFactory -import org.readium.r2.shared.util.resource.FileZipArchiveFactory -import org.readium.r2.shared.util.zip.StreamingZipArchiveFactory +import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider import org.readium.r2.streamer.PublicationFactory /** @@ -43,24 +41,21 @@ class Readium(context: Context) { mediaTypeRetriever = mediaTypeRetriever ) - private val archiveFactory = CompositeArchiveFactory( - FileZipArchiveFactory(mediaTypeRetriever), - StreamingZipArchiveFactory(mediaTypeRetriever) + private val archiveProviders = listOf( + StreamingZipArchiveProvider(mediaTypeRetriever) ) private val resourceFactory = CompositeResourceFactory( FileResourceFactory(mediaTypeRetriever), - CompositeResourceFactory( - ContentResourceFactory(context.contentResolver), - HttpResourceFactory(httpClient) - ) + ContentResourceFactory(context.contentResolver), + HttpResourceFactory(httpClient) ) val assetRetriever = AssetRetriever( mediaTypeRetriever, resourceFactory, - archiveFactory, - context.contentResolver + context.contentResolver, + archiveProviders ) val downloadManager = AndroidDownloadManager( diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt index 002c0c6f68..a9cf438d43 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt @@ -45,7 +45,11 @@ import org.readium.r2.testapp.utils.EventChannel import org.readium.r2.testapp.utils.createViewModelFactory import timber.log.Timber -@OptIn(ExperimentalDecorator::class, ExperimentalCoroutinesApi::class) +@OptIn( + ExperimentalDecorator::class, + ExperimentalCoroutinesApi::class, + ExperimentalReadiumApi::class +) class ReaderViewModel( private val bookId: Long, private val readerRepository: ReaderRepository, From 9b8075560b68c906f91c1c79f9227cf83849d626 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 6 Nov 2023 13:51:51 +0100 Subject: [PATCH 07/86] Refactor container and resource --- .../navigator/PdfiumDocumentFragment.kt | 5 +- .../pdfium/navigator/PdfiumEngineProvider.kt | 4 +- .../pspdfkit/document/PsPdfKitDocument.kt | 11 +- .../pspdfkit/document/ResourceDataProvider.kt | 20 +- .../navigator/PsPdfKitDocumentFragment.kt | 4 +- .../navigator/PsPdfKitEngineProvider.kt | 4 +- .../readium/r2/lcp/LcpContentProtection.kt | 88 +++--- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 118 ++++++-- .../java/org/readium/r2/lcp/LcpService.kt | 3 +- .../container/ContainerLicenseContainer.kt | 4 +- .../readium/r2/lcp/service/LicensesService.kt | 5 +- .../org/readium/r2/navigator/Navigator.kt | 4 +- .../r2/navigator/epub/WebViewServer.kt | 14 +- .../r2/navigator/media/ExoMediaPlayer.kt | 6 +- .../readium/r2/navigator/media/MediaPlayer.kt | 4 +- .../r2/navigator/media/MediaService.kt | 6 +- .../media/tts/session/TtsSessionAdapter.kt | 8 +- .../java/org/readium/r2/opds/OPDS2Parser.kt | 2 +- .../r2/shared/publication/Contributor.kt | 7 +- .../org/readium/r2/shared/publication/Link.kt | 12 +- .../readium/r2/shared/publication/Locator.kt | 16 +- .../readium/r2/shared/publication/Manifest.kt | 8 - .../readium/r2/shared/publication/Metadata.kt | 17 -- .../r2/shared/publication/Properties.kt | 5 +- .../r2/shared/publication/Publication.kt | 35 ++- .../publication/PublicationCollection.kt | 10 +- .../readium/r2/shared/publication/Subject.kt | 7 +- .../AdeptFallbackContentProtection.kt | 65 +++-- .../protection/ContentProtection.kt | 34 ++- .../ContentProtectionSchemeRetriever.kt | 39 ++- .../LcpFallbackContentProtection.kt | 133 +++++---- .../services/ContentProtectionService.kt | 32 ++- .../publication/services/CoverService.kt | 6 +- .../publication/services/PositionsService.kt | 2 +- .../services/content/ContentService.kt | 6 +- .../iterators/HtmlResourceContentIterator.kt | 2 +- .../iterators/PublicationContentIterator.kt | 6 +- .../services/search/StringSearchService.kt | 7 +- .../java/org/readium/r2/shared/util/Error.kt | 21 -- .../readium/r2/shared/util/FilesystemError.kt | 34 +++ .../java/org/readium/r2/shared/util/Try.kt | 13 +- .../shared/util/archive/ArchiveProperties.kt | 66 +++++ .../ArchiveProvider.kt} | 24 +- .../FileZipArchiveProvider.kt | 63 ++-- .../{resource => archive}/ZipContainer.kt | 142 +++------ .../org/readium/r2/shared/util/asset/Asset.kt | 5 +- .../r2/shared/util/asset/AssetError.kt | 85 ------ .../r2/shared/util/asset/AssetRetriever.kt | 211 ++------------ .../util/asset/ContentResourceFactory.kt | 49 ++++ .../shared/util/asset/FileResourceFactory.kt | 42 +++ .../{http => asset}/HttpResourceFactory.kt | 5 +- .../{resource => asset}/ResourceFactory.kt | 3 +- .../DataSource.kt => data/Blob.kt} | 15 +- .../BlobInputStream.kt} | 44 ++- .../readium/r2/shared/util/data/Container.kt | 75 +++++ .../ContentBlob.kt} | 73 ++--- .../DataSourceDecoder.kt => data/Decoding.kt} | 36 ++- .../FileResource.kt => data/FileBlob.kt} | 68 +---- .../readium/r2/shared/util/data/ReadError.kt | 71 +++++ .../RoutingClosedContainer.kt} | 19 +- .../android/AndroidDownloadManager.kt | 15 +- .../r2/shared/util/http/DefaultHttpClient.kt | 4 +- .../r2/shared/util/http/HttpContainer.kt | 27 +- .../r2/shared/util/http/HttpResource.kt | 43 ++- .../util/mediatype/DefaultMediaTypeSniffer.kt | 47 +++ .../util/mediatype/MediaTypeRetriever.kt | 142 +++++---- .../shared/util/mediatype/MediaTypeSniffer.kt | 270 +++++++++--------- .../util/mediatype/MediaTypeSnifferContent.kt | 216 -------------- .../shared/util/resource/ArchiveProvider.kt | 11 - .../util/resource/BlobResourceAdapters.kt | 47 +++ .../shared/util/resource/BufferingResource.kt | 7 +- .../r2/shared/util/resource/BytesResource.kt | 17 +- .../r2/shared/util/resource/Container.kt | 83 ------ .../util/resource/DirectoryContainer.kt | 44 +-- .../shared/util/resource/FallbackResource.kt | 16 +- .../util/resource/FileChannelResource.kt | 104 ------- .../r2/shared/util/resource/LazyResource.kt | 20 +- .../r2/shared/util/resource/MediaTypeExt.kt | 120 -------- .../r2/shared/util/resource/Resource.kt | 147 ++-------- .../shared/util/resource/ResourceContainer.kt | 78 +++++ .../util/resource/ResourceDataSource.kt | 81 ------ .../util/resource/ResourceInputStream.kt | 26 -- .../util/resource/SynchronizedResource.kt | 10 +- .../util/resource/TransformingContainer.kt | 24 +- .../util/resource/TransformingResource.kt | 19 +- .../content/ResourceContentExtractor.kt | 37 ++- .../r2/shared/util/zip/ChannelZipContainer.kt | 73 +++-- .../r2/shared/util/zip/DatasourceChannel.kt | 12 +- .../readium/r2/shared/util/zip/HttpChannel.kt | 169 ----------- .../util/zip/StreamingZipArchiveProvider.kt | 61 ++-- .../LcpFallbackContentProtectionTest.kt | 1 - .../publication/protection/TestContainer.kt | 2 +- .../publication/services/CoverServiceTest.kt | 4 +- .../util/mediatype/MediaTypeRetrieverTest.kt | 2 +- .../util/resource/BufferingResourceTest.kt | 3 +- .../util/resource/DirectoryContainerTest.kt | 11 +- ...rcePropertiesTest.kt => PropertiesTest.kt} | 5 +- .../util/resource/ResourceInputStreamTest.kt | 3 +- .../shared/util/resource/ZipContainerTest.kt | 4 +- .../readium/r2/streamer/ParserAssetFactory.kt | 84 +++--- .../readium/r2/streamer/PublicationFactory.kt | 76 +++-- .../r2/streamer/parser/PublicationParser.kt | 73 +---- .../r2/streamer/parser/audio/AudioParser.kt | 16 +- .../streamer/parser/epub/ClockValueParser.kt | 4 +- .../r2/streamer/parser/epub/EpubParser.kt | 84 ++++-- .../parser/epub/EpubPositionsService.kt | 16 +- .../r2/streamer/parser/image/ImageParser.kt | 16 +- .../r2/streamer/parser/pdf/PdfParser.kt | 15 +- .../parser/readium/LcpdfPositionsService.kt | 10 +- .../parser/readium/ReadiumWebPubParser.kt | 50 ++-- .../streamer/extensions/ContainerEntryTest.kt | 3 +- .../parser/epub/EpubPositionsServiceTest.kt | 9 +- .../streamer/parser/image/ImageParserTest.kt | 8 +- .../java/org/readium/r2/testapp/Readium.kt | 6 +- .../readium/r2/testapp/domain/Bookshelf.kt | 4 +- .../readium/r2/testapp/domain/ImportError.kt | 4 +- .../r2/testapp/domain/PublicationError.kt | 1 - .../r2/testapp/domain/PublicationRetriever.kt | 6 +- .../r2/testapp/reader/ReaderRepository.kt | 1 - .../r2/testapp/reader/ReaderViewModel.kt | 6 +- 120 files changed, 1943 insertions(+), 2477 deletions(-) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/FilesystemError.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource/ArchiveFactory.kt => archive/ArchiveProvider.kt} (69%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource => archive}/FileZipArchiveProvider.kt (58%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource => archive}/ZipContainer.kt (52%) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetError.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/{http => asset}/HttpResourceFactory.kt (85%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource => asset}/ResourceFactory.kt (94%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{datasource/DataSource.kt => data/Blob.kt} (68%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{datasource/DataSourceInputStream.kt => data/BlobInputStream.kt} (77%) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource/ContentResource.kt => data/ContentBlob.kt} (58%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{datasource/DataSourceDecoder.kt => data/Decoding.kt} (75%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource/FileResource.kt => data/FileBlob.kt} (57%) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource/RoutingContainer.kt => data/RoutingClosedContainer.kt} (71%) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProvider.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeExt.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceDataSource.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceInputStream.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/zip/HttpChannel.kt rename readium/shared/src/test/java/org/readium/r2/shared/util/resource/{ResourcePropertiesTest.kt => PropertiesTest.kt} (93%) diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt index e439c1c42b..87b8ab0af6 100644 --- a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt @@ -26,9 +26,8 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.SingleJob import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.e +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.resource.ResourceError import timber.log.Timber @ExperimentalReadiumApi @@ -41,7 +40,7 @@ public class PdfiumDocumentFragment internal constructor( ) : PdfDocumentFragment() { internal interface Listener { - fun onResourceLoadFailed(href: Url, error: ResourceError) + fun onResourceLoadFailed(href: Url, error: ReadError) fun onConfigurePdfView(configurator: PDFView.Configurator) fun onTap(point: PointF): Boolean } diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt index 9845a33ba4..c37de3ef6d 100644 --- a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt @@ -19,7 +19,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Metadata import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.data.ReadError /** * Main component to use the PDF navigator with the PDFium adapter. @@ -49,7 +49,7 @@ public class PdfiumEngineProvider( initialPageIndex = input.pageIndex, initialSettings = input.settings, listener = object : PdfiumDocumentFragment.Listener { - override fun onResourceLoadFailed(href: Url, error: ResourceError) { + override fun onResourceLoadFailed(href: Url, error: ReadError) { input.navigatorListener?.onResourceLoadFailed(href, error) } diff --git a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt index 7619d8eb12..6a519827a0 100644 --- a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt @@ -21,10 +21,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.publication.ReadingProgression import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.resource.ResourceTry import timber.log.Timber @@ -36,6 +36,7 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory = open(context, DocumentSource(ResourceDataProvider(resource), password)) + // FIXME : error handling is too rough private suspend fun open(context: Context, documentSource: DocumentSource): ResourceTry = withContext(Dispatchers.IO) { try { @@ -43,13 +44,13 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory Unit = { Timber.e(it) } + private val onResourceError: (ReadError) -> Unit = { Timber.e(it) } ) : DataProvider { private val resource = // PSPDFKit accesses the resource from multiple threads. resource.synchronized() - private val length: Long = runBlocking { - resource.length() - .getOrElse { - onResourceError(it) - DataProvider.FILE_SIZE_UNKNOWN.toLong() - } + private val length by lazy { + runBlocking { + resource.length() + .getOrElse { + onResourceError(it) + DataProvider.FILE_SIZE_UNKNOWN.toLong() + } + } } override fun getSize(): Long = length diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt index 2724475da1..4b479fffc2 100644 --- a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt @@ -55,7 +55,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.isProtected import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.pdf.cachedIn -import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.resource.ResourceTry import timber.log.Timber @@ -69,7 +69,7 @@ public class PsPdfKitDocumentFragment internal constructor( ) : PdfDocumentFragment() { internal interface Listener { - fun onResourceLoadFailed(href: Url, error: ResourceError) + fun onResourceLoadFailed(href: Url, error: ReadError) fun onConfigurePdfView(builder: PdfConfiguration.Builder): PdfConfiguration.Builder fun onTap(point: PointF): Boolean } diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt index bb25d3f806..5c3eb1be1d 100644 --- a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt @@ -20,7 +20,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Metadata import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.data.ReadError /** * Main component to use the PDF navigator with PSPDFKit. @@ -50,7 +50,7 @@ public class PsPdfKitEngineProvider( initialPageIndex = input.pageIndex, initialSettings = input.settings, listener = object : PsPdfKitDocumentFragment.Listener { - override fun onResourceLoadFailed(href: Url, error: ResourceError) { + override fun onResourceLoadFailed(href: Url, error: ReadError) { input.navigatorListener?.onResourceLoadFailed(href, error) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index b8542f15a7..9b6a32a969 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -13,21 +13,23 @@ 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.util.AbsoluteUrl +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.TransformingContainer internal class LcpContentProtection( private val lcpService: LcpService, private val authentication: LcpAuthenticating, - private val assetRetriever: AssetRetriever + private val assetRetriever: AssetRetriever, + private val mediaTypeRetriever: MediaTypeRetriever ) : ContentProtection { override val scheme: ContentProtection.Scheme = @@ -35,14 +37,14 @@ internal class LcpContentProtection( override suspend fun supports( asset: Asset - ): Boolean = + ): Try = lcpService.isLcpProtected(asset) override suspend fun open( asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { return when (asset) { is Asset.Container -> openPublication(asset, credentials, allowUserInteraction) is Asset.Resource -> openLicense(asset, credentials, allowUserInteraction) @@ -53,7 +55,7 @@ internal class LcpContentProtection( asset: Asset.Container, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { val license = retrieveLicense(asset, credentials, allowUserInteraction) return createResultAsset(asset, license) } @@ -73,11 +75,11 @@ internal class LcpContentProtection( private fun createResultAsset( asset: Asset.Container, license: Try - ): Try { + ): Try { val serviceFactory = LcpContentProtectionService .createFactory(license.getOrNull(), license.failureOrNull()) - val decryptor = LcpDecryptor(license.getOrNull()) + val decryptor = LcpDecryptor(license.getOrNull(), mediaTypeRetriever) val container = TransformingContainer(asset.container, decryptor::transform) @@ -103,7 +105,7 @@ internal class LcpContentProtection( licenseAsset: Asset.Resource, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction) val licenseDoc = license.getOrNull()?.license @@ -113,24 +115,32 @@ internal class LcpContentProtection( LicenseDocument(it) } catch (e: Exception) { return Try.failure( - AssetError.InvalidAsset( - "Failed to read the LCP license document", - cause = ThrowableError(e) + ContentProtection.Error.AccessError( + ReadError.Content( + MessageError( + "Failed to read the LCP license document", + cause = ThrowableError(e) + ) + ) ) ) } } .getOrElse { return Try.failure( - it.wrap() + ContentProtection.Error.AccessError(it) ) } val link = licenseDoc.publicationLink val url = (link.url() as? AbsoluteUrl) ?: return Try.failure( - AssetError.InvalidAsset( - "The LCP license document does not contain a valid link to the publication" + ContentProtection.Error.AccessError( + ReadError.Content( + MessageError( + "The LCP license document does not contain a valid link to the publication" + ) + ) ) ) @@ -150,7 +160,13 @@ internal class LcpContentProtection( if (it is Asset.Container) { Try.success((it)) } else { - Try.failure(AssetError.UnsupportedAsset()) + Try.failure( + ContentProtection.Error.UnsupportedAsset( + MessageError( + "LCP license points to an unsupported publication." + ) + ) + ) } } } @@ -158,43 +174,13 @@ internal class LcpContentProtection( return asset.flatMap { createResultAsset(it, license) } } - private fun ResourceError.wrap(): AssetError = - when (this) { - is ResourceError.Forbidden -> - AssetError.Forbidden(this) - is ResourceError.NotFound -> - AssetError.NotFound(this) - is ResourceError.Other -> - AssetError.Unknown(this) - is ResourceError.OutOfMemory -> - AssetError.OutOfMemory(this) - is ResourceError.Filesystem -> - AssetError.Filesystem(cause) - is ResourceError.InvalidContent -> - AssetError.InvalidAsset(this) - is ResourceError.Network -> - AssetError.Network(cause) - } - - private fun AssetRetriever.Error.wrap(): AssetError = + private fun AssetRetriever.Error.wrap(): ContentProtection.Error = when (this) { is AssetRetriever.Error.ArchiveFormatNotSupported -> - AssetError.UnsupportedAsset(this) - is AssetRetriever.Error.Forbidden -> - AssetError.Forbidden(this) - is AssetRetriever.Error.InvalidAsset -> - AssetError.InvalidAsset(this) - is AssetRetriever.Error.NotFound -> - AssetError.NotFound(this) - is AssetRetriever.Error.OutOfMemory -> - AssetError.OutOfMemory(this) + ContentProtection.Error.UnsupportedAsset(this) + is AssetRetriever.Error.AccessError -> + ContentProtection.Error.AccessError(cause) is AssetRetriever.Error.SchemeNotSupported -> - AssetError.UnsupportedAsset(this) - is AssetRetriever.Error.Unknown -> - AssetError.Unknown(this) - is AssetRetriever.Error.Filesystem -> - AssetError.Filesystem(cause) - is AssetRetriever.Error.Network -> - AssetError.Network(cause) + ContentProtection.Error.UnsupportedAsset(this) } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 2e1b01f74d..133c105356 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -19,26 +19,31 @@ 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.assertSuccess +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.resource.Container +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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.FailureResource import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceError -import org.readium.r2.shared.util.resource.ResourceTry +import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.resource.TransformingResource import org.readium.r2.shared.util.resource.flatMap -import org.readium.r2.shared.util.resource.flatMapCatching +import org.readium.r2.shared.util.tryRecover /** * Decrypts a resource protected with LCP. */ internal class LcpDecryptor( val license: LcpLicense?, + private val mediaTypeRetriever: MediaTypeRetriever, var encryptionData: Map = emptyMap() ) { fun transform(resource: Resource): Resource { - if (resource !is Container.Entry) { + if (resource !is ResourceEntry) { return resource } @@ -52,14 +57,24 @@ internal class LcpDecryptor( } when { - license == null -> FailureResource(ResourceError.Forbidden()) - encryption.isDeflated || !encryption.isCbcEncrypted -> FullLcpResource( - resource, - encryption, - license - ) - - else -> CbcLcpResource(resource, encryption, license) + license == null -> + FailureResource( + ReadError.Content() + ) + encryption.isDeflated || !encryption.isCbcEncrypted -> + FullLcpResource( + resource, + encryption, + license, + mediaTypeRetriever + ) + else -> + CbcLcpResource( + resource, + encryption, + license, + mediaTypeRetriever + ) } } } @@ -71,15 +86,33 @@ internal class LcpDecryptor( * resource, for example when the resource is deflated before encryption. */ private class FullLcpResource( - resource: Resource, + private val resource: ResourceEntry, private val encryption: Encryption, - private val license: LcpLicense + private val license: LcpLicense, + private val mediaTypeRetriever: MediaTypeRetriever ) : TransformingResource(resource) { - override suspend fun transform(data: ResourceTry): ResourceTry = + override val source: AbsoluteUrl? = + null + override suspend fun mediaType(): Try = + mediaTypeRetriever + .retrieve( + hints = MediaTypeHints(fileExtension = resource.url.extension), + blob = this + ) + .tryRecover { error -> + when (error) { + is MediaTypeSnifferError.DataAccess -> + Try.failure(error.cause) + MediaTypeSnifferError.NotRecognized -> + Try.success(MediaType.BINARY) + } + } + + override suspend fun transform(data: Try): Try = license.decryptFully(data, encryption.isDeflated) - override suspend fun length(): ResourceTry = + override suspend fun length(): Try = encryption.originalLength?.let { Try.success(it) } ?: super.length() } @@ -90,9 +123,10 @@ internal class LcpDecryptor( * Supports random access for byte range requests, but the resource MUST NOT be deflated. */ private class CbcLcpResource( - private val resource: Resource, + private val resource: ResourceEntry, private val encryption: Encryption, - private val license: LcpLicense + private val license: LcpLicense, + private val mediaTypeRetriever: MediaTypeRetriever ) : Resource by resource { override val source: AbsoluteUrl? = null @@ -102,7 +136,7 @@ internal class LcpDecryptor( val data: ByteArray = ByteArray(3 * AES_BLOCK_SIZE) ) - private lateinit var _length: ResourceTry + private lateinit var _length: Try /* * Decryption needs to look around the data strictly matching the content to decipher. @@ -115,8 +149,22 @@ internal class LcpDecryptor( */ private val _cache: Cache = Cache() + override suspend fun mediaType(): Try = + mediaTypeRetriever + .retrieve( + hints = MediaTypeHints(fileExtension = resource.url.extension), + blob = this + ).tryRecover { error -> + when (error) { + is MediaTypeSnifferError.DataAccess -> + Try.failure(error.cause) + MediaTypeSnifferError.NotRecognized -> + Try.success(MediaType.BINARY) + } + } + /** Plain text size. */ - override suspend fun length(): ResourceTry { + override suspend fun length(): Try { if (::_length.isInitialized) { return _length } @@ -127,12 +175,16 @@ internal class LcpDecryptor( return _length } - private suspend fun lengthFromPadding(): ResourceTry { + private suspend fun lengthFromPadding(): Try { val length = resource.length() .getOrElse { return Try.failure(it) } if (length < 2 * AES_BLOCK_SIZE) { - return Try.failure(ResourceError.InvalidContent("Invalid CBC-encrypted stream.")) + return Try.failure( + ReadError.Content( + MessageError("Invalid CBC-encrypted stream.") + ) + ) } val readOffset = length - (2 * AES_BLOCK_SIZE) @@ -142,11 +194,12 @@ internal class LcpDecryptor( val decryptedBytes = license.decrypt(bytes) .getOrElse { return Try.failure( - ResourceError.InvalidContent( - "Can't decrypt trailing size of CBC-encrypted stream" + ReadError.Content( + MessageError("Can't decrypt trailing size of CBC-encrypted stream") ) ) } + check(decryptedBytes.size == AES_BLOCK_SIZE) val adjustedLength = length - @@ -156,7 +209,7 @@ internal class LcpDecryptor( return Try.success(adjustedLength) } - override suspend fun read(range: LongRange?): ResourceTry { + override suspend fun read(range: LongRange?): Try { if (range == null) { return license.decryptFully(resource.read(), isDeflated = false) } @@ -191,7 +244,7 @@ internal class LcpDecryptor( val bytes = license.decrypt(encryptedData) .getOrElse { return Try.failure( - ResourceError.InvalidContent( + ReadError.Content( MessageError( "Can't decrypt the content for resource with key: ${resource.source}", ThrowableError(it) @@ -221,7 +274,7 @@ internal class LcpDecryptor( return Try.success(bytes.sliceArray(sliceStart until sliceEnd)) } - private suspend fun getEncryptedData(range: LongRange): ResourceTry { + private suspend fun getEncryptedData(range: LongRange): Try { val cacheStartIndex = _cache.startIndex ?.takeIf { cacheStart -> val cacheEnd = cacheStart + _cache.data.size @@ -245,13 +298,16 @@ internal class LcpDecryptor( } } -private suspend fun LcpLicense.decryptFully(data: ResourceTry, isDeflated: Boolean): ResourceTry = - data.flatMapCatching { encryptedData -> +private suspend fun LcpLicense.decryptFully( + data: Try, + isDeflated: Boolean +): Try = + data.flatMap { encryptedData -> // Decrypts the resource. var bytes = decrypt(encryptedData) .getOrElse { return Try.failure( - ResourceError.InvalidContent( + ReadError.Content( MessageError("Failed to decrypt the resource", ThrowableError(it)) ) ) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt index 652b493063..22aec3ce0f 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt @@ -30,6 +30,7 @@ import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever @@ -47,7 +48,7 @@ public interface LcpService { /** * Returns if the asset is a LCP license document or a publication protected by LCP. */ - public suspend fun isLcpProtected(asset: Asset): Boolean + public suspend fun isLcpProtected(asset: Asset): Try /** * Acquires a protected publication from a standalone LCPL's bytes. diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt index 7faaf238d8..2965542bd9 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt @@ -9,9 +9,9 @@ package org.readium.r2.lcp.license.container import kotlinx.coroutines.runBlocking import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.ResourceError /** * Access to a License Document stored in a read-only container. @@ -28,7 +28,7 @@ internal class ContainerLicenseContainer( .read() .mapFailure { when (it) { - is ResourceError.NotFound -> + is ReadError.NotFound -> LcpException.Container.FileNotFound(entryUrl) else -> LcpException.Container.ReadFailed(entryUrl) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index c385125397..49e37fa789 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -38,6 +38,7 @@ import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry @@ -63,11 +64,11 @@ internal class LicensesService( return isLcpProtected(asset) } - override suspend fun isLcpProtected(asset: Asset): Boolean = + override suspend fun isLcpProtected(asset: Asset): Try = tryOr(false) { when (asset) { is Asset.Resource -> - asset.mediaType == MediaType.LCP_LICENSE_DOCUMENT + Try.success(asset.mediaType == MediaType.LCP_LICENSE_DOCUMENT) is Asset.Container -> { createLicenseContainer(context, asset.container, asset.mediaType).read() true diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt index 3cb0fb9b5b..fd156b5085 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt @@ -11,7 +11,7 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.data.ReadError /** * Base interface for a navigator rendering a publication. @@ -59,7 +59,7 @@ public interface Navigator { /** * Called when a publication resource failed to be loaded. */ - public fun onResourceLoadFailed(href: Url, error: ResourceError) {} + public fun onResourceLoadFailed(href: Url, error: ReadError) {} /** * Called when the navigator jumps to an explicit location, which might break the linear diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt index 3530a02b8f..54ff53627a 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt @@ -18,11 +18,12 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.AccessException +import org.readium.r2.shared.util.data.BlobInputStream +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.http.HttpHeaders import org.readium.r2.shared.util.http.HttpRange import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceError -import org.readium.r2.shared.util.resource.ResourceInputStream /** * Serves the publication resources and application assets in the EPUB navigator web views. @@ -33,7 +34,7 @@ internal class WebViewServer( private val publication: Publication, servedAssets: List, private val disableSelectionWhenProtected: Boolean, - private val onResourceLoadFailed: (Url, ResourceError) -> Unit + private val onResourceLoadFailed: (Url, ReadError) -> Unit ) { companion object { val publicationBaseHref = AbsoluteUrl("https://readium/publication/")!! @@ -110,15 +111,16 @@ internal class WebViewServer( 200, "OK", headers, - ResourceInputStream(resource) + BlobInputStream(resource, ::AccessException) ) } else { // Byte range request - val stream = ResourceInputStream(resource) + val stream = BlobInputStream(resource, ::AccessException) val length = stream.available() val longRange = range.toLongRange(length.toLong()) 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() + // headers["Content-Length"] = (longRange.last - longRange.first + 1).toString() + // Weirdly, the WebView will call itself stream.skip to skip to the requested range. return WebResourceResponse( link.mediaType?.toString(), null, diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt index 2653d3bd8d..c0553fc477 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt @@ -57,7 +57,7 @@ import org.readium.r2.shared.publication.indexOfFirstWithHref import org.readium.r2.shared.util.NetworkError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.toUri import timber.log.Timber @@ -202,9 +202,9 @@ public class ExoMediaPlayer( } override fun onPlayerError(error: PlaybackException) { - var resourceError: ResourceError? = error.asInstance() + var resourceError: ReadError? = error.asInstance() if (resourceError == null && (error.cause as? HttpDataSource.HttpDataSourceException)?.cause is UnknownHostException) { - resourceError = ResourceError.Network( + resourceError = ReadError.Network( NetworkError.Offline(ThrowableError(error.cause!!)) ) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt index f1f2feb9ef..320fa79e6c 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaPlayer.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationId -import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.data.ReadError /** * Media player compatible with Android's MediaSession and handling the playback for @@ -59,7 +59,7 @@ public interface MediaPlayer { * Called when a resource failed to be loaded, for example because the Internet connection * is offline and the resource is streamed. */ - public fun onResourceLoadFailed(link: Link, error: ResourceError) + public fun onResourceLoadFailed(link: Link, error: ReadError) /** * Creates the [NotificationMetadata] for the given resource [link]. diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt index 813cf4cace..10647f8b42 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaService.kt @@ -34,7 +34,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationId import org.readium.r2.shared.publication.services.cover import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.data.ReadError import timber.log.Timber /** @@ -105,7 +105,7 @@ public open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by * * You should present the exception to the user. */ - public open fun onResourceLoadFailed(link: Link, error: ResourceError) {} + public open fun onResourceLoadFailed(link: Link, error: ReadError) {} /** * Override to control which app can access the MediaSession through the MediaBrowserService. @@ -211,7 +211,7 @@ public open class MediaService : MediaBrowserServiceCompat(), CoroutineScope by this@MediaService.onPlayerStopped() } - override fun onResourceLoadFailed(link: Link, error: ResourceError) { + override fun onResourceLoadFailed(link: Link, error: ReadError) { this@MediaService.onResourceLoadFailed(link, error) } } diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt index 42e8a90908..7dd9477df7 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt @@ -44,7 +44,7 @@ import org.readium.navigator.media.tts.TtsEngine import org.readium.navigator.media.tts.TtsPlayer import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.ErrorException -import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.data.ReadError /** * Adapts the [TtsPlayer] to media3 [Player] interface. @@ -924,11 +924,11 @@ internal class TtsSessionAdapter( } is TtsPlayer.State.Error.ContentError -> { val errorCode = when (error) { - is ResourceError.NotFound -> + is ReadError.NotFound -> ERROR_CODE_IO_BAD_HTTP_STATUS - is ResourceError.Forbidden -> + is ReadError.Forbidden -> ERROR_CODE_DRM_DISALLOWED_OPERATION - is ResourceError.Network -> + is ReadError.Network -> ERROR_CODE_IO_NETWORK_CONNECTION_FAILED else -> ERROR_CODE_UNSPECIFIED diff --git a/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt b/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt index 8553aa6ed9..92d5c7df18 100644 --- a/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt +++ b/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt @@ -273,7 +273,7 @@ public class OPDS2Parser { } private fun parsePublication(json: JSONObject, baseUrl: Url): Publication? = - Manifest.fromJSON(json, mediaTypeRetriever = mediaTypeRetriever) + Manifest.fromJSON(json, mediaTypeSniffer = mediaTypeRetriever) // Self link takes precedence over the given `baseUrl`. ?.let { it.normalizeHrefsToBase(it.linkWithRel("self")?.href?.resolve() ?: baseUrl) } ?.let { Publication(it) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Contributor.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Contributor.kt index 335d62e5e5..dfa3d7c02b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Contributor.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Contributor.kt @@ -21,7 +21,6 @@ import org.readium.r2.shared.extensions.parseObjects import org.readium.r2.shared.extensions.putIfNotEmpty import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.logging.log -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever /** * Contributor Object for the Readium Web Publication Manifest. @@ -84,7 +83,6 @@ public data class Contributor( */ public fun fromJSON( json: Any?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null ): Contributor? { json ?: return null @@ -108,7 +106,6 @@ public data class Contributor( position = jsonObject.optNullableDouble("position"), links = Link.fromJSONArray( jsonObject.optJSONArray("links"), - mediaTypeRetriever, warnings ) ) @@ -121,7 +118,6 @@ public data class Contributor( */ public fun fromJSONArray( json: Any?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null ): List { return when (json) { @@ -129,13 +125,12 @@ public data class Contributor( listOf(json).mapNotNull { fromJSON( it, - mediaTypeRetriever, warnings ) } is JSONArray -> - json.parseObjects { fromJSON(it, mediaTypeRetriever, warnings) } + json.parseObjects { fromJSON(it, warnings) } else -> emptyList() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Link.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Link.kt index 1e45962a56..4460536f04 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Link.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Link.kt @@ -25,7 +25,6 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.logging.log import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever /** * Link Object for the Readium Web Publication Manifest. @@ -162,7 +161,6 @@ public data class Link( */ public fun fromJSON( json: JSONObject?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null ): Link? { json ?: return null @@ -170,7 +168,7 @@ public data class Link( return Link( href = parseHref(json, warnings) ?: return null, mediaType = json.optNullableString("type") - ?.let { mediaTypeRetriever.retrieve(it) }, + ?.let { MediaType(it) }, title = json.optNullableString("title"), rels = json.optStringsFromArrayOrSingle("rel").toSet(), properties = Properties.fromJSON(json.optJSONObject("properties")), @@ -180,12 +178,10 @@ public data class Link( duration = json.optPositiveDouble("duration"), languages = json.optStringsFromArrayOrSingle("language"), alternates = fromJSONArray( - json.optJSONArray("alternate"), - mediaTypeRetriever + json.optJSONArray("alternate") ), children = fromJSONArray( - json.optJSONArray("children"), - mediaTypeRetriever + json.optJSONArray("children") ) ) } @@ -232,13 +228,11 @@ public data class Link( */ public fun fromJSONArray( json: JSONArray?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null ): List { return json.parseObjects { fromJSON( it as? JSONObject, - mediaTypeRetriever, warnings ) } 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 c055dd8b6b..c9605fec5d 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 @@ -22,7 +22,6 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.logging.log import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever /** * Represents a precise location in a publication in a format that can be stored and shared. @@ -203,10 +202,9 @@ public data class Locator( */ public fun fromJSON( json: JSONObject?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null ): Locator? = - fromJSON(json, mediaTypeRetriever, warnings, withLegacyHref = false) + fromJSON(json, warnings, withLegacyHref = false) /** * Creates a [Locator] from its legacy JSON representation. @@ -217,15 +215,13 @@ public data class Locator( @DelicateReadiumApi public fun fromLegacyJSON( json: JSONObject?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null ): Locator? = - fromJSON(json, mediaTypeRetriever, warnings, withLegacyHref = true) + fromJSON(json, warnings, withLegacyHref = true) @OptIn(DelicateReadiumApi::class) private fun fromJSON( json: JSONObject?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null, withLegacyHref: Boolean = false ): Locator? { @@ -254,7 +250,7 @@ public data class Locator( return Locator( href = url, - mediaType = mediaTypeRetriever.retrieve(mediaType), + mediaType = mediaType, title = json.optNullableString("title"), locations = Locations.fromJSON(json.optJSONObject("locations")), text = Text.fromJSON(json.optJSONObject("text")) @@ -263,10 +259,9 @@ public data class Locator( public fun fromJSONArray( json: JSONArray?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null ): List { - return json.parseObjects { fromJSON(it as? JSONObject, mediaTypeRetriever, warnings) } + return json.parseObjects { fromJSON(it as? JSONObject, warnings) } } } } @@ -341,19 +336,16 @@ public data class LocatorCollection( public fun fromJSON( json: JSONObject?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null ): LocatorCollection { return LocatorCollection( metadata = Metadata.fromJSON(json?.optJSONObject("metadata"), warnings), links = Link.fromJSONArray( json?.optJSONArray("links"), - mediaTypeRetriever, warnings = warnings ), locators = Locator.fromJSONArray( json?.optJSONArray("locators"), - mediaTypeRetriever, warnings ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Manifest.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Manifest.kt index e1d284d6f4..ccadeeb927 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Manifest.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Manifest.kt @@ -20,7 +20,6 @@ import org.readium.r2.shared.util.logging.ConsoleWarningLogger import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.logging.log import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever /** * Holds the metadata of a Readium publication, as described in the Readium Web Publication Manifest. @@ -154,7 +153,6 @@ public data class Manifest( */ public fun fromJSON( json: JSONObject?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = ConsoleWarningLogger() ): Manifest? { json ?: return null @@ -163,7 +161,6 @@ public data class Manifest( val metadata = Metadata.fromJSON( json.remove("metadata") as? JSONObject, - mediaTypeRetriever, warnings ) if (metadata == null) { @@ -173,7 +170,6 @@ public data class Manifest( val links = Link.fromJSONArray( json.remove("links") as? JSONArray, - mediaTypeRetriever, warnings ) @@ -181,28 +177,24 @@ public data class Manifest( val readingOrderJSON = (json.remove("readingOrder") ?: json.remove("spine")) as? JSONArray val readingOrder = Link.fromJSONArray( readingOrderJSON, - mediaTypeRetriever, warnings ) .filter { it.mediaType != null } val resources = Link.fromJSONArray( json.remove("resources") as? JSONArray, - mediaTypeRetriever, warnings ) .filter { it.mediaType != null } val tableOfContents = Link.fromJSONArray( json.remove("toc") as? JSONArray, - mediaTypeRetriever, warnings ) // Parses subcollections from the remaining JSON properties. val subcollections = PublicationCollection.collectionsFromJSON( json, - mediaTypeRetriever, warnings ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt index 43eb062b6e..bdb09fcaa1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Metadata.kt @@ -27,7 +27,6 @@ import org.readium.r2.shared.publication.presentation.presentation import org.readium.r2.shared.util.Language import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.logging.log -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever /** * https://readium.org/webpub-manifest/schema/metadata.schema.json @@ -256,7 +255,6 @@ public data class Metadata( */ public fun fromJSON( json: JSONObject?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null ): Metadata? { json ?: return null @@ -279,72 +277,58 @@ public data class Metadata( val localizedSortAs = LocalizedString.fromJSON(json.remove("sortAs"), warnings) val subjects = Subject.fromJSONArray( json.remove("subject"), - mediaTypeRetriever, warnings ) val authors = Contributor.fromJSONArray( json.remove("author"), - mediaTypeRetriever, warnings ) val translators = Contributor.fromJSONArray( json.remove("translator"), - mediaTypeRetriever, warnings ) val editors = Contributor.fromJSONArray( json.remove("editor"), - mediaTypeRetriever, warnings ) val artists = Contributor.fromJSONArray( json.remove("artist"), - mediaTypeRetriever, warnings ) val illustrators = Contributor.fromJSONArray( json.remove("illustrator"), - mediaTypeRetriever, warnings ) val letterers = Contributor.fromJSONArray( json.remove("letterer"), - mediaTypeRetriever, warnings ) val pencilers = Contributor.fromJSONArray( json.remove("penciler"), - mediaTypeRetriever, warnings ) val colorists = Contributor.fromJSONArray( json.remove("colorist"), - mediaTypeRetriever, warnings ) val inkers = Contributor.fromJSONArray( json.remove("inker"), - mediaTypeRetriever, warnings ) val narrators = Contributor.fromJSONArray( json.remove("narrator"), - mediaTypeRetriever, warnings ) val contributors = Contributor.fromJSONArray( json.remove("contributor"), - mediaTypeRetriever, warnings ) val publishers = Contributor.fromJSONArray( json.remove("publisher"), - mediaTypeRetriever, warnings ) val imprints = Contributor.fromJSONArray( json.remove("imprint"), - mediaTypeRetriever, warnings ) val readingProgression = ReadingProgression( @@ -366,7 +350,6 @@ public data class Metadata( val value = belongsToJson.get(key) belongsTo[key] = Collection.fromJSONArray( value, - mediaTypeRetriever, warnings ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt index 7b30dd849a..024d02233a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt @@ -16,7 +16,6 @@ import org.json.JSONObject import org.readium.r2.shared.JSONable import org.readium.r2.shared.extensions.JSONParceler import org.readium.r2.shared.extensions.toMap -import org.readium.r2.shared.util.resource.Resource /** * Properties associated to the linked resource. @@ -55,8 +54,8 @@ public data class Properties( */ public operator fun get(key: String): Any? = otherProperties[key] - internal fun toResourceProperties(): Resource.Properties = - Resource.Properties(otherProperties) + internal fun toResourceProperties(): Properties = + Properties(otherProperties) public companion object { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index fd05f16aa5..31ee97fd88 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -29,12 +29,11 @@ import org.readium.r2.shared.publication.services.content.ContentService import org.readium.r2.shared.publication.services.search.SearchService import org.readium.r2.shared.util.Closeable import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.EmptyContainer import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.EmptyContainer import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceError -import org.readium.r2.shared.util.resource.fallback +import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.resource.withMediaType internal typealias ServiceFactory = (Publication.Service.Context) -> Publication.Service? @@ -49,6 +48,8 @@ internal typealias ServiceFactory = (Publication.Service.Context) -> Publication */ public typealias PublicationId = String +public typealias PublicationContainer = ClosedContainer + /** * The Publication shared model is the entry-point for all the metadata and services * related to a Readium publication. @@ -61,7 +62,7 @@ public typealias PublicationId = String */ public class Publication( manifest: Manifest, - private val container: Container = EmptyContainer(), + private val container: PublicationContainer = EmptyContainer(), private val servicesBuilder: ServicesBuilder = ServicesBuilder(), @Deprecated( "Migrate to the new Settings API (see migration guide)", @@ -194,27 +195,23 @@ public class Publication( /** * Returns the resource targeted by the given non-templated [link]. */ - public fun get(link: Link): Resource = + public fun get(link: Link): Resource? = get(link.url(), link.mediaType) /** * Returns the resource targeted by the given [href]. */ - public fun get(href: Url): Resource = + public fun get(href: Url): Resource? = get(href, linkWithHref(href)?.mediaType) - private fun get(href: Url, mediaType: MediaType?): Resource { + private fun get(href: Url, mediaType: MediaType?): Resource? { services.services.forEach { service -> service.get(href)?.let { return it } } - return container.get(href) - .fallback { error -> - if (error is ResourceError.NotFound) { - // Try again after removing query and fragment. - container.get(href.removeQuery().removeFragment()) - } else { - null - } - } + val entry = container.get(href) + ?: container.get(href.removeQuery().removeFragment()) // Try again after removing query and fragment. + ?: return null + + return entry .withMediaType(mediaType) } @@ -357,7 +354,7 @@ public class Publication( */ public class Context( public val manifest: Manifest, - public val container: Container, + public val container: PublicationContainer, public val services: PublicationServicesHolder ) @@ -500,7 +497,7 @@ public class Publication( */ public class Builder( public var manifest: Manifest, - public var container: Container, + public var container: PublicationContainer, public var servicesBuilder: ServicesBuilder = ServicesBuilder() ) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/PublicationCollection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/PublicationCollection.kt index 167b43cf5a..201e5ee6ee 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/PublicationCollection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/PublicationCollection.kt @@ -21,7 +21,6 @@ import org.readium.r2.shared.extensions.putIfNotEmpty import org.readium.r2.shared.extensions.toMap import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.logging.log -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever /** * Core Collection Model @@ -54,7 +53,6 @@ public data class PublicationCollection( */ public fun fromJSON( json: Any?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null ): PublicationCollection? { json ?: return null @@ -68,20 +66,18 @@ public data class PublicationCollection( is JSONObject -> { links = Link.fromJSONArray( json.remove("links") as? JSONArray, - mediaTypeRetriever, warnings ) metadata = (json.remove("metadata") as? JSONObject)?.toMap() subcollections = collectionsFromJSON( json, - mediaTypeRetriever, warnings ) } // Parses an array of links. is JSONArray -> { - links = Link.fromJSONArray(json, mediaTypeRetriever, warnings) + links = Link.fromJSONArray(json, warnings) } else -> { @@ -112,7 +108,6 @@ public data class PublicationCollection( */ public fun collectionsFromJSON( json: JSONObject, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null ): Map> { val collections = mutableMapOf>() @@ -120,7 +115,7 @@ public data class PublicationCollection( val subJSON = json.get(role) // Parses a list of links or a single collection object. - val collection = fromJSON(subJSON, mediaTypeRetriever, warnings) + val collection = fromJSON(subJSON, warnings) if (collection != null) { collections.getOrPut(role) { mutableListOf() }.add(collection) @@ -130,7 +125,6 @@ public data class PublicationCollection( subJSON.mapNotNull { fromJSON( it, - mediaTypeRetriever, warnings ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Subject.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Subject.kt index dc3134bc53..7159e09662 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Subject.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Subject.kt @@ -19,7 +19,6 @@ import org.readium.r2.shared.extensions.parseObjects import org.readium.r2.shared.extensions.putIfNotEmpty import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.logging.log -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever /** * https://github.com/readium/webpub-manifest/tree/master/contexts/default#subjects @@ -76,7 +75,6 @@ public data class Subject( */ public fun fromJSON( json: Any?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null ): Subject? { json ?: return null @@ -99,7 +97,6 @@ public data class Subject( code = jsonObject.optNullableString("code"), links = Link.fromJSONArray( jsonObject.optJSONArray("links"), - mediaTypeRetriever, warnings ) ) @@ -112,7 +109,6 @@ public data class Subject( */ public fun fromJSONArray( json: Any?, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), warnings: WarningLogger? = null ): List { return when (json) { @@ -120,13 +116,12 @@ public data class Subject( listOf(json).mapNotNull { fromJSON( it, - mediaTypeRetriever, warnings ) } is JSONArray -> - json.parseObjects { fromJSON(it, mediaTypeRetriever, warnings) } + json.parseObjects { fromJSON(it, warnings) } else -> emptyList() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt index ae4395e2b7..f5b50d9c4f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt @@ -9,14 +9,15 @@ package org.readium.r2.shared.publication.protection import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.protection.ContentProtection.Scheme import org.readium.r2.shared.publication.services.contentProtectionServiceFactory +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.asset.AssetError +import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.readAsXml +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.readAsXml -import org.readium.r2.shared.util.xml.ElementNode /** * [ContentProtection] implementation used as a fallback by the Streamer to detect Adept DRM, @@ -27,9 +28,9 @@ public class AdeptFallbackContentProtection : ContentProtection { override val scheme: Scheme = Scheme.Adept - override suspend fun supports(asset: Asset): Boolean { + override suspend fun supports(asset: Asset): Try { if (asset !is Asset.Container) { - return false + return Try.success(false) } return isAdept(asset) @@ -39,10 +40,12 @@ public class AdeptFallbackContentProtection : ContentProtection { asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { if (asset !is Asset.Container) { return Try.failure( - AssetError.UnsupportedAsset("A container asset was expected.") + ContentProtection.Error.UnsupportedAsset( + MessageError("A container asset was expected.") + ) ) } @@ -58,27 +61,37 @@ public class AdeptFallbackContentProtection : ContentProtection { return Try.success(protectedFile) } - private suspend fun isAdept(asset: Asset.Container): Boolean { + private suspend fun isAdept(asset: Asset.Container): Try { if (!asset.mediaType.matches(MediaType.EPUB)) { - return false + return Try.success(false) } - val rightsXml = asset.container.get(Url("META-INF/rights.xml")!!) - .readAsXmlOrNull() + asset.container.get(Url("META-INF/encryption.xml")!!) + ?.readAsXml() + ?.getOrElse { + when (it) { + is DecoderError.DecodingError -> + return Try.success(false) + is DecoderError.DataAccess -> + return Try.failure(it.cause) + } + }?.get("EncryptedData", EpubEncryption.ENC) + ?.flatMap { it.get("KeyInfo", EpubEncryption.SIG) } + ?.flatMap { it.get("resource", "http://ns.adobe.com/adept") } + ?.takeIf { it.isNotEmpty() } + ?.let { return Try.success(true) } - val encryptionXml = asset.container.get(Url("META-INF/encryption.xml")!!) - .readAsXmlOrNull() - - return encryptionXml != null && ( - rightsXml?.namespace == "http://ns.adobe.com/adept" || - encryptionXml - .get("EncryptedData", EpubEncryption.ENC) - .flatMap { it.get("KeyInfo", EpubEncryption.SIG) } - .flatMap { it.get("resource", "http://ns.adobe.com/adept") } - .isNotEmpty() - ) + return asset.container.get(Url("META-INF/rights.xml")!!) + ?.readAsXml() + ?.getOrElse { + when (it) { + is DecoderError.DecodingError -> + return Try.success(false) + is DecoderError.DataAccess -> + return Try.failure(it.cause) + } + }?.takeIf { it.namespace == "http://ns.adobe.com/adept" } + ?.let { Try.success(true) } + ?: Try.success(false) } } - -private suspend inline fun Resource.readAsXmlOrNull(): ElementNode? = - readAsXml().getOrNull() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index d80da52cc4..81695e3948 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -10,15 +10,25 @@ package org.readium.r2.shared.publication.protection import androidx.annotation.StringRes +import kotlin.Any +import kotlin.Boolean +import kotlin.Deprecated +import kotlin.DeprecationLevel +import kotlin.Int +import kotlin.String +import kotlin.Throwable +import kotlin.Unit import org.readium.r2.shared.R import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.LocalizedString import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.ContentProtectionService +import org.readium.r2.shared.util.Error as BaseError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.AssetError +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Container +import org.readium.r2.shared.util.resource.ResourceEntry /** * Bridge between a Content Protection technology and the Readium toolkit. @@ -29,6 +39,20 @@ import org.readium.r2.shared.util.resource.Container */ public interface ContentProtection { + public sealed class Error( + override val message: String, + override val cause: BaseError? + ) : BaseError { + + public class AccessError( + override val cause: org.readium.r2.shared.util.data.ReadError + ) : Error("An error occurred while trying to read asset.", cause) + + public class UnsupportedAsset( + override val cause: BaseError? + ) : Error("Asset is not supported.", cause) + } + public val scheme: Scheme /** @@ -36,7 +60,7 @@ public interface ContentProtection { */ public suspend fun supports( asset: org.readium.r2.shared.util.asset.Asset - ): Boolean + ): Try /** * Attempts to unlock a potentially protected publication asset. @@ -48,7 +72,7 @@ public interface ContentProtection { asset: org.readium.r2.shared.util.asset.Asset, credentials: String?, allowUserInteraction: Boolean - ): Try + ): Try /** * Holds the result of opening an [Asset] with a [ContentProtection]. @@ -61,7 +85,7 @@ public interface ContentProtection { */ public data class Asset( val mediaType: MediaType, - val container: Container, + val container: ClosedContainer, val onCreatePublication: Publication.Builder.() -> Unit = {} ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt index d340d08bad..274e1ed53a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt @@ -6,24 +6,45 @@ package org.readium.r2.shared.publication.protection -import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import kotlin.String +import kotlin.let +import kotlin.takeIf +import org.readium.r2.shared.util.Error as BaseError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse /** * Retrieves [ContentProtection] schemes of assets. */ public class ContentProtectionSchemeRetriever( - contentProtections: List, - mediaTypeRetriever: MediaTypeRetriever + contentProtections: List ) { private val contentProtections: List = contentProtections + listOf( - LcpFallbackContentProtection(mediaTypeRetriever), + LcpFallbackContentProtection(), AdeptFallbackContentProtection() ) - public suspend fun retrieve(asset: org.readium.r2.shared.util.asset.Asset): ContentProtection.Scheme? = - contentProtections - .firstOrNull { it.supports(asset) } - ?.scheme + public sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? + ) : BaseError { + + public object NoContentProtectionFound : + Error("No content protection recognized the given asset.", null) + + public class AccessError(cause: org.readium.r2.shared.util.Error?) : + Error("An error occurred while trying to read asset.", cause) + } + + public suspend fun retrieve(asset: org.readium.r2.shared.util.asset.Asset): Try { + for (protection in contentProtections) { + protection.supports(asset) + .getOrElse { return Try.failure(Error.AccessError(it)) } + .takeIf { it } + ?.let { return Try.success(protection.scheme) } + } + + return Try.failure(Error.NoContentProtectionFound) + } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index 0f94265ac5..845edce97a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -6,55 +6,55 @@ package org.readium.r2.shared.publication.protection -import org.json.JSONObject import org.readium.r2.shared.InternalReadiumApi -import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.encryption.encryption import org.readium.r2.shared.publication.protection.ContentProtection.Scheme import org.readium.r2.shared.publication.services.contentProtectionServiceFactory +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.asset.AssetError +import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.readAsJson +import org.readium.r2.shared.util.data.readAsRwpm +import org.readium.r2.shared.util.data.readAsXml +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever -import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.readAsJson -import org.readium.r2.shared.util.resource.readAsXml -import org.readium.r2.shared.util.xml.ElementNode +import org.readium.r2.shared.util.resource.ResourceContainer /** * [ContentProtection] implementation used as a fallback by the Streamer to detect LCP DRM * if it is not supported by the app. */ @InternalReadiumApi -public class LcpFallbackContentProtection( - private val mediaTypeRetriever: MediaTypeRetriever -) : ContentProtection { +public class LcpFallbackContentProtection : ContentProtection { override val scheme: Scheme = Scheme.Lcp - override suspend fun supports(asset: Asset): Boolean = + override suspend fun supports(asset: Asset): Try = when (asset) { is Asset.Container -> isLcpProtected( asset.container, asset.mediaType ) - is Asset.Resource -> asset.mediaType.matches( - MediaType.LCP_LICENSE_DOCUMENT - ) + is Asset.Resource -> + Try.success( + asset.mediaType.matches(MediaType.LCP_LICENSE_DOCUMENT) + ) } override suspend fun open( asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { if (asset !is Asset.Container) { return Try.failure( - AssetError.UnsupportedAsset("A container asset was expected.") + ContentProtection.Error.UnsupportedAsset( + MessageError("A container asset was expected.") + ) ) } @@ -70,49 +70,74 @@ public class LcpFallbackContentProtection( return Try.success(protectedFile) } - private suspend fun isLcpProtected(container: Container, mediaType: MediaType): Boolean { - return when { - mediaType.matches(MediaType.READIUM_WEBPUB) || - mediaType.matches(MediaType.LCP_PROTECTED_PDF) || - mediaType.matches(MediaType.LCP_PROTECTED_AUDIOBOOK) -> { - if (container.get(Url("license.lcpl")!!).readAsJsonOrNull() != null) { - return true - } + private suspend fun isLcpProtected(container: ResourceContainer, mediaType: MediaType): Try { + val isReadiumWebpub = mediaType.matches(MediaType.READIUM_WEBPUB) || + mediaType.matches(MediaType.LCP_PROTECTED_PDF) || + mediaType.matches(MediaType.LCP_PROTECTED_AUDIOBOOK) - val manifestAsJson = container.get(Url("manifest.json")!!).readAsJsonOrNull() - ?: return false + val isEpub = mediaType.matches(MediaType.EPUB) - val manifest = Manifest.fromJSON( - manifestAsJson, - mediaTypeRetriever = mediaTypeRetriever - ) - ?: return false + if (!isReadiumWebpub && !isEpub) { + return Try.success(false) + } - return manifest - .readingOrder - .any { it.properties.encryption?.scheme == "http://readium.org/2014/01/lcp" } - } - mediaType.matches(MediaType.EPUB) -> { - if (container.get(Url("META-INF/license.lcpl")!!).readAsJsonOrNull() != null) { - return true + container.get(Url("license.lcpl")!!) + ?.readAsJson() + ?.getOrElse { + when (it) { + is DecoderError.DataAccess -> + Try.failure(it.cause.cause) + is DecoderError.DecodingError -> + return Try.success(false) } + } - val encryptionXml = container.get(Url("META-INF/encryption.xml")!!).readAsXmlOrNull() - ?: return false + return when { + isReadiumWebpub -> hasLcpSchemeInManifest(container) + else -> hasLcpSchemeInEncryptionXml(container) // isEpub + } + } - return encryptionXml - .get("EncryptedData", EpubEncryption.ENC) - .flatMap { it.get("KeyInfo", EpubEncryption.SIG) } - .flatMap { it.get("RetrievalMethod", EpubEncryption.SIG) } - .any { it.getAttr("URI") == "license.lcpl#/encryption/content_key" } + private suspend fun hasLcpSchemeInManifest(container: ResourceContainer): Try { + val manifest = container.get(Url("manifest.json")!!) + ?.readAsRwpm() + ?.getOrElse { + when (it) { + is DecoderError.DataAccess -> + return Try.failure(ReadError.Content(it)) + is DecoderError.DecodingError -> + return Try.success(false) + } } - else -> false - } + ?: return Try.success(false) + + val manifestHasLcpScheme = manifest + .readingOrder + .any { it.properties.encryption?.scheme == "http://readium.org/2014/01/lcp" } + + return Try.success(manifestHasLcpScheme) } -} -private suspend inline fun Resource.readAsJsonOrNull(): JSONObject? = - readAsJson().getOrNull() + private suspend fun hasLcpSchemeInEncryptionXml(container: ResourceContainer): Try { + val encryptionXml = container + .get(Url("META-INF/encryption.xml")!!) + ?.readAsXml() + ?.getOrElse { + when (it) { + is DecoderError.DataAccess -> + return Try.failure(ReadError.Content(it.cause.cause)) + is DecoderError.DecodingError -> + return Try.failure(ReadError.Content(it.cause)) + } + } + ?: return Try.success(false) + + val hasLcpScheme = encryptionXml + .get("EncryptedData", EpubEncryption.ENC) + .flatMap { it.get("KeyInfo", EpubEncryption.SIG) } + .flatMap { it.get("RetrievalMethod", EpubEncryption.SIG) } + .any { it.getAttr("URI") == "license.lcpl#/encryption/content_key" } -private suspend inline fun Resource.readAsXmlOrNull(): ElementNode? = - readAsXml().getOrNull() + return Try.success(hasLcpScheme) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt index 624ad4fe3a..0df6f2faaf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt @@ -23,12 +23,16 @@ import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.NetworkError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.FailureResource +import org.readium.r2.shared.util.resource.FailureResourceEntry import org.readium.r2.shared.util.resource.LazyResource import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceError +import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.resource.StringResource +import org.readium.r2.shared.util.resource.toResourceEntry /** * Provides information about a publication's content protection and manages user rights. @@ -295,20 +299,22 @@ private sealed class RouteHandler { override fun acceptRequest(url: Url): Boolean = url.path == path - override fun handleRequest(url: Url, service: ContentProtectionService): Resource = - LazyResource { handleRequestAsync(url, service) } + override fun handleRequest(url: Url, service: ContentProtectionService): ResourceEntry = + LazyResource { handleRequestAsync(url, service) }.toResourceEntry(url) - private suspend fun handleRequestAsync(url: Url, service: ContentProtectionService): Resource { + private suspend fun handleRequestAsync(url: Url, service: ContentProtectionService): ResourceEntry { val query = url.query val text = query.firstNamedOrNull("text") - ?: return FailureResource( - ResourceError.Network( + ?: return FailureResourceEntry( + url, + ReadError.Network( NetworkError.BadRequest("'text' parameter is required") ) ) val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() - ?: return FailureResource( - ResourceError.Network( + ?: return FailureResourceEntry( + url, + ReadError.Network( NetworkError.BadRequest("If present, 'peek' must be true or false") ) ) @@ -316,7 +322,7 @@ private sealed class RouteHandler { val copyAllowed = with(service.rights) { if (peek) canCopy(text) else copy(text) } return if (!copyAllowed) { - FailureResource(ResourceError.Forbidden()) + FailureResource(ReadError.Network(HttpError(HttpError.Kind.Forbidden))) } else { StringResource("true", MediaType.JSON) } @@ -342,20 +348,20 @@ private sealed class RouteHandler { val query = url.query val pageCountString = query.firstNamedOrNull("pageCount") ?: return FailureResource( - ResourceError.Network( + ReadError.Network( NetworkError.BadRequest("'pageCount' parameter is required") ) ) val pageCount = pageCountString.toIntOrNull()?.takeIf { it >= 0 } ?: return FailureResource( - ResourceError.Network( + ReadError.Network( NetworkError.BadRequest("'pageCount' must be a positive integer") ) ) val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() ?: return FailureResource( - ResourceError.Network( + ReadError.Network( NetworkError.BadRequest("if present, 'peek' must be true or false") ) ) @@ -371,7 +377,7 @@ private sealed class RouteHandler { } return if (!printAllowed) { - FailureResource(ResourceError.Forbidden()) + FailureResource(ReadError.Network(NetworkError.Forbidden())) } else { StringResource("true", mediaType = MediaType.JSON) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index edc7817fe1..3ad4e604c8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -19,12 +19,12 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.BytesResource import org.readium.r2.shared.util.resource.FailureResource import org.readium.r2.shared.util.resource.LazyResource import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceError /** * Provides an easy access to a bitmap version of the publication cover. @@ -60,7 +60,7 @@ public interface CoverService : Publication.Service { private suspend fun Publication.coverFromManifest(): Bitmap? { for (link in linksWithRel("cover")) { - val data = get(link).read().getOrNull() ?: continue + val data = get(link)?.read()?.getOrNull() ?: continue return BitmapFactory.decodeByteArray(data, 0, data.size) ?: continue } return null @@ -113,7 +113,7 @@ public abstract class GeneratedCoverService : CoverService { if (png == null) { FailureResource( - ResourceError.InvalidContent( + ReadError.Content( MessageError("Unable to convert cover to PNG.") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt index 3229c1e4eb..226283d7b8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt @@ -23,10 +23,10 @@ import org.readium.r2.shared.publication.firstWithMediaType import org.readium.r2.shared.toJSON import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.readAsString import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.StringResource -import org.readium.r2.shared.util.resource.readAsString private val positionsMediaType = MediaType("application/vnd.readium.position-list+json")!! diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentService.kt index 65be164989..aa017f1556 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentService.kt @@ -8,10 +8,10 @@ package org.readium.r2.shared.publication.services.content import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.* +import org.readium.r2.shared.publication.PublicationContainer import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.publication.services.content.iterators.PublicationContentIterator import org.readium.r2.shared.publication.services.content.iterators.ResourceContentIteratorFactory -import org.readium.r2.shared.util.resource.Container /** * Provides a way to extract the raw [Content] of a [Publication]. @@ -52,7 +52,7 @@ public var Publication.ServicesBuilder.contentServiceFactory: ServiceFactory? @ExperimentalReadiumApi public class DefaultContentService( private val manifest: Manifest, - private val container: Container, + private val container: PublicationContainer, private val services: PublicationServicesHolder, private val resourceContentIteratorFactories: List ) : ContentService { @@ -76,7 +76,7 @@ public class DefaultContentService( private inner class ContentImpl( val manifest: Manifest, - val container: Container, + val container: PublicationContainer, val services: PublicationServicesHolder, val start: Locator? ) : Content { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt index 7aaa5fccc7..486215a17b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt @@ -33,10 +33,10 @@ import org.readium.r2.shared.publication.services.content.Content.VideoElement import org.readium.r2.shared.publication.services.positionsByReadingOrder import org.readium.r2.shared.util.Language import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.readAsString import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.readAsString import org.readium.r2.shared.util.use import org.readium.r2.shared.util.w import timber.log.Timber diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt index 825b810738..ec1e4cc268 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt @@ -10,11 +10,11 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.publication.PublicationContainer import org.readium.r2.shared.publication.PublicationServicesHolder import org.readium.r2.shared.publication.indexOfFirstWithHref import org.readium.r2.shared.publication.services.content.Content import org.readium.r2.shared.util.Either -import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.Resource /** @@ -53,7 +53,7 @@ public fun interface ResourceContentIteratorFactory { @ExperimentalReadiumApi public class PublicationContentIterator( private val manifest: Manifest, - private val container: Container, + private val container: PublicationContainer, private val services: PublicationServicesHolder, private val startLocator: Locator?, private val resourceContentIteratorFactories: List @@ -164,7 +164,7 @@ public class PublicationContentIterator( private suspend fun loadIteratorAt(index: Int, location: LocatorOrProgression): IndexedIterator? { val link = manifest.readingOrder[index] val locator = location.toLocator(link) ?: return null - val resource = container.get(link.url()) + val resource = container.get(link.url()) ?: return null return resourceContentIteratorFactories .firstNotNullOfOrNull { factory -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt index f549e87bad..442974b4e1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt @@ -24,7 +24,6 @@ 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.getOrElse -import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.content.DefaultResourceContentExtractorFactory import org.readium.r2.shared.util.resource.content.ResourceContentExtractor import timber.log.Timber @@ -42,7 +41,7 @@ import timber.log.Timber @ExperimentalReadiumApi public class StringSearchService( private val manifest: Manifest, - private val container: Container, + private val container: PublicationContainer, private val services: PublicationServicesHolder, private val language: String?, private val snippetLength: Int, @@ -92,7 +91,7 @@ public class StringSearchService( private inner class Iterator( val manifest: Manifest, - val container: Container, + val container: PublicationContainer, val query: String, val options: Options, val locale: Locale @@ -117,7 +116,7 @@ public class StringSearchService( val link = manifest.readingOrder[index] val text = container.get(link.url()) - .let { extractorFactory.createExtractor(it)?.extractText(it) } + ?.let { extractorFactory.createExtractor(it)?.extractText(it) } ?.getOrElse { return Try.failure(SearchError.ResourceError(it)) } if (text == null) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt index 008307dd29..e2b567d648 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt @@ -50,27 +50,6 @@ public class ErrorException( public val error: Error ) : Exception(error.message, error.cause?.let { ErrorException(it) }) -public fun Try.assertSuccess(): S = - when (this) { - is Try.Success -> - value - is Try.Failure -> - throw IllegalStateException( - "Try was excepted to contain a success.", - value as? Throwable - ) - } - -public class FilesystemError( - override val cause: Error? = null -) : Error { - - public constructor(exception: Exception) : this(ThrowableError(exception)) - - override val message: String = - "An unexpected error occurred on the filesystem." -} - // FIXME: to improve @InternalReadiumApi public fun Timber.Forest.e(error: Error, message: String? = null) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/FilesystemError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/FilesystemError.kt new file mode 100644 index 0000000000..7bd3cbf782 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/FilesystemError.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util + +public sealed class FilesystemError( + override val message: String, + override val cause: Error? = null +) : Error { + + public class NotFound( + cause: Error? + ) : FilesystemError("File not found.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + public class Forbidden( + cause: Error? + ) : FilesystemError("You are not allowed to access this file.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + public class Unknown( + cause: Error? + ) : FilesystemError("An unexpected error occurred on the filesystem.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt index 8a1ef681c9..afb4743859 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt @@ -143,8 +143,19 @@ public inline fun Try.flatMap(transform: (value: S) -> Try * Returns the encapsulated result of the given transform function applied to the encapsulated value * if this instance represents failure or the original encapsulated value if it is success. */ -public inline fun Try.tryRecover(transform: (exception: F) -> Try): Try = +public inline fun Try.tryRecover(transform: (exception: F) -> Try): Try = when (this) { is Try.Success -> Try.success(value) is Try.Failure -> transform(value) } + +public fun Try.assertSuccess(): S = + when (this) { + is Try.Success -> + value + is Try.Failure -> + throw IllegalStateException( + "Try was excepted to contain a success.", + value as? Throwable + ) + } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt new file mode 100644 index 0000000000..a1920221bf --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.archive + +import org.json.JSONObject +import org.readium.r2.shared.JSONable +import org.readium.r2.shared.extensions.optNullableBoolean +import org.readium.r2.shared.extensions.optNullableLong +import org.readium.r2.shared.extensions.toMap +import org.readium.r2.shared.util.resource.Resource + +/** + * Holds information about how the resource is stored in the archive. + * + * @param entryLength The length of the entry stored in the archive. It might be a compressed length + * if the entry is deflated. + * @param isEntryCompressed Indicates whether the entry was compressed before being stored in the + * archive. + */ +public data class ArchiveProperties( + val entryLength: Long, + val isEntryCompressed: Boolean +) : JSONable { + + override fun toJSON(): JSONObject = JSONObject().apply { + put("entryLength", entryLength) + put("isEntryCompressed", isEntryCompressed) + } + + public companion object { + public fun fromJSON(json: JSONObject?): ArchiveProperties? { + json ?: return null + + val entryLength = json.optNullableLong("entryLength") + val isEntryCompressed = json.optNullableBoolean("isEntryCompressed") + if (entryLength == null || isEntryCompressed == null) { + return null + } + return ArchiveProperties( + entryLength = entryLength, + isEntryCompressed = isEntryCompressed + ) + } + } +} + +private const val ARCHIVE_KEY = "archive" + +public val Resource.Properties.archive: ArchiveProperties? + get() = (this[ARCHIVE_KEY] as? Map<*, *>) + ?.let { ArchiveProperties.fromJSON(JSONObject(it)) } + +public var Resource.Properties.Builder.archive: ArchiveProperties? + get() = (this[ARCHIVE_KEY] as? Map<*, *>) + ?.let { ArchiveProperties.fromJSON(JSONObject(it)) } + set(value) { + if (value == null) { + remove(ARCHIVE_KEY) + } else { + put(ARCHIVE_KEY, value.toJSON().toMap()) + } + } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt similarity index 69% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt index 293769b23d..49bf51ddbc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt @@ -4,14 +4,22 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.archive import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.mediatype.MediaTypeSniffer +import org.readium.r2.shared.util.resource.ResourceContainer +import org.readium.r2.shared.util.resource.ResourceEntry + +public interface ArchiveProvider : MediaTypeSniffer, ArchiveFactory /** - * A factory to create [Container]s from archive [Resource]s. + * A factory to create a [ResourceContainer]s from archive [Blob]s. * */ public interface ArchiveFactory { @@ -33,17 +41,17 @@ public interface ArchiveFactory { ) : Error("Resource is not supported.", cause) public class ResourceError( - override val cause: org.readium.r2.shared.util.resource.ResourceError + override val cause: ReadError ) : Error("An error occurred while attempting to read the resource.", cause) } /** - * Creates a new archive [Container] to access the entries of the given archive. + * Creates a new archive [ResourceContainer] to access the entries of the given archive. */ public suspend fun create( - resource: Resource, + resource: Blob, password: String? = null - ): Try + ): Try, Error> } public class CompositeArchiveFactory( @@ -53,9 +61,9 @@ public class CompositeArchiveFactory( public constructor(vararg factories: ArchiveFactory) : this(factories.toList()) override suspend fun create( - resource: Resource, + resource: Blob, password: String? - ): Try { + ): Try, ArchiveFactory.Error> { for (factory in factories) { factory.create(resource, password) .getOrElse { error -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt similarity index 58% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveProvider.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt index 4806317f24..7d42940781 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt @@ -4,9 +4,10 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.archive import java.io.File +import java.io.FileNotFoundException import java.io.IOException import java.util.zip.ZipException import java.util.zip.ZipFile @@ -16,19 +17,21 @@ import org.readium.r2.shared.util.FilesystemError import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferContentError import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.mediatype.ResourceMediaTypeSnifferContent +import org.readium.r2.shared.util.resource.ResourceEntry /** * An [ArchiveFactory] to open local ZIP files with Java's [ZipFile]. */ public class FileZipArchiveProvider( - private val mediaTypeRetriever: MediaTypeRetriever + private val mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever() ) : ArchiveProvider { override fun sniffHints(hints: MediaTypeHints): Try { @@ -41,8 +44,8 @@ public class FileZipArchiveProvider( return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { - val file = resource.source?.toFile() + override suspend fun sniffBlob(blob: Blob): Try { + val file = blob.source?.toFile() ?: return Try.Failure(MediaTypeSnifferError.NotRecognized) return withContext(Dispatchers.IO) { @@ -53,20 +56,20 @@ public class FileZipArchiveProvider( Try.failure(MediaTypeSnifferError.NotRecognized) } catch (e: SecurityException) { Try.failure( - MediaTypeSnifferError.SourceError( - MediaTypeSnifferContentError.Forbidden(ThrowableError(e)) + MediaTypeSnifferError.DataAccess( + ReadError.Filesystem(FilesystemError.Forbidden(e)) ) ) } catch (e: IOException) { Try.failure( - MediaTypeSnifferError.SourceError( - MediaTypeSnifferContentError.Filesystem(FilesystemError(e)) + MediaTypeSnifferError.DataAccess( + ReadError.Filesystem(FilesystemError.Unknown(e)) ) ) } catch (e: Exception) { Try.failure( - MediaTypeSnifferError.SourceError( - MediaTypeSnifferContentError.Unknown(ThrowableError(e)) + MediaTypeSnifferError.DataAccess( + ReadError.Other(ThrowableError(e)) ) ) } @@ -74,9 +77,9 @@ public class FileZipArchiveProvider( } override suspend fun create( - resource: Resource, + resource: Blob, password: String? - ): Try { + ): Try, ArchiveFactory.Error> { if (password != null) { return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) } @@ -95,17 +98,41 @@ public class FileZipArchiveProvider( } // Internal for testing purpose - internal suspend fun open(file: File): Try = + internal suspend fun open(file: File): Try, ArchiveFactory.Error> = withContext(Dispatchers.IO) { try { val archive = JavaZipContainer(ZipFile(file), file, mediaTypeRetriever) Try.success(archive) + } catch (e: FileNotFoundException) { + Try.failure( + ArchiveFactory.Error.ResourceError( + ReadError.Filesystem(FilesystemError.NotFound(e)) + ) + ) } catch (e: ZipException) { - Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.InvalidContent(e))) + Try.failure( + ArchiveFactory.Error.ResourceError( + ReadError.Content(e) + ) + ) } catch (e: SecurityException) { - Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.Forbidden(e))) + Try.failure( + ArchiveFactory.Error.ResourceError( + ReadError.Filesystem(FilesystemError.Forbidden(e)) + ) + ) } catch (e: IOException) { - Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.Filesystem(e))) + Try.failure( + ArchiveFactory.Error.ResourceError( + ReadError.Filesystem(FilesystemError.Unknown(e)) + ) + ) + } catch (e: Exception) { + Try.failure( + ArchiveFactory.Error.ResourceError( + ReadError.Other(e) + ) + ) } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt similarity index 52% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt index eb067ec0ad..d93f512f84 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.archive import java.io.File import java.io.IOException @@ -12,130 +12,65 @@ import java.util.zip.ZipEntry import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.json.JSONObject -import org.readium.r2.shared.JSONable -import org.readium.r2.shared.extensions.optNullableBoolean -import org.readium.r2.shared.extensions.optNullableLong import org.readium.r2.shared.extensions.readFully -import org.readium.r2.shared.extensions.toMap import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.FilesystemError import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.toUrl - -/** - * Holds information about how the resource is stored in the archive. - * - * @param entryLength The length of the entry stored in the archive. It might be a compressed length - * if the entry is deflated. - * @param isEntryCompressed Indicates whether the entry was compressed before being stored in the - * archive. - */ -public data class ArchiveProperties( - val entryLength: Long, - val isEntryCompressed: Boolean -) : JSONable { - - override fun toJSON(): JSONObject = JSONObject().apply { - put("entryLength", entryLength) - put("isEntryCompressed", isEntryCompressed) - } - - public companion object { - public fun fromJSON(json: JSONObject?): ArchiveProperties? { - json ?: return null - - val entryLength = json.optNullableLong("entryLength") - val isEntryCompressed = json.optNullableBoolean("isEntryCompressed") - if (entryLength == null || isEntryCompressed == null) { - return null - } - return ArchiveProperties( - entryLength = entryLength, - isEntryCompressed = isEntryCompressed - ) - } - } -} - -private const val ARCHIVE_KEY = "archive" - -public val Resource.Properties.archive: ArchiveProperties? - get() = (this[ARCHIVE_KEY] as? Map<*, *>) - ?.let { ArchiveProperties.fromJSON(JSONObject(it)) } - -public var Resource.Properties.Builder.archive: ArchiveProperties? - get() = (this[ARCHIVE_KEY] as? Map<*, *>) - ?.let { ArchiveProperties.fromJSON(JSONObject(it)) } - set(value) { - if (value == null) { - remove(ARCHIVE_KEY) - } else { - put(ARCHIVE_KEY, value.toJSON().toMap()) - } - } +import org.readium.r2.shared.util.tryRecover internal class JavaZipContainer( private val archive: ZipFile, file: File, private val mediaTypeRetriever: MediaTypeRetriever -) : Container { +) : ClosedContainer { - private inner class FailureEntry(override val url: Url) : Container.Entry { + private inner class Entry(override val url: Url, private val entry: ZipEntry) : + ResourceEntry { override val source: AbsoluteUrl? = null - override suspend fun mediaType(): ResourceTry = + override suspend fun mediaType(): Try = mediaTypeRetriever.retrieve( hints = MediaTypeHints(fileExtension = url.extension), - content = ResourceMediaTypeSnifferContent(this) - ).toResourceTry() - - override suspend fun properties(): ResourceTry = - Try.failure(ResourceError.NotFound()) - - override suspend fun length(): ResourceTry = - Try.failure(ResourceError.NotFound()) - - override suspend fun read(range: LongRange?): ResourceTry = - Try.failure(ResourceError.NotFound()) - - override suspend fun close() { - } - } - - private inner class Entry(override val url: Url, private val entry: ZipEntry) : Container.Entry { - - override val source: AbsoluteUrl? = null - - override suspend fun mediaType(): ResourceTry = - mediaTypeRetriever.retrieve( - hints = MediaTypeHints(fileExtension = url.extension), - content = ResourceMediaTypeSnifferContent(this) - ).toResourceTry() + blob = this + ).tryRecover { error -> + when (error) { + is MediaTypeSnifferError.DataAccess -> + Try.failure(error.cause) + MediaTypeSnifferError.NotRecognized -> + Try.success(MediaType.BINARY) + } + } - override suspend fun properties(): ResourceTry = - ResourceTry.success( + override suspend fun properties(): Try = + Try.success( Resource.Properties { archive = ArchiveProperties( entryLength = compressedLength - ?: length().getOrElse { return ResourceTry.failure(it) }, + ?: length().getOrElse { return Try.failure(it) }, isEntryCompressed = compressedLength != null ) } ) - override suspend fun length(): Try = + override suspend fun length(): Try = entry.size.takeUnless { it == -1L } ?.let { Try.success(it) } - ?: Try.failure(ResourceError.Other(Exception("Unsupported operation"))) + ?: Try.failure(ReadError.Other(Exception("Unsupported operation"))) private val compressedLength: Long? = if (entry.method == ZipEntry.STORED || entry.method == -1) { @@ -144,7 +79,7 @@ internal class JavaZipContainer( entry.compressedSize.takeUnless { it == -1L } } - override suspend fun read(range: LongRange?): Try = + override suspend fun read(range: LongRange?): Try = try { withContext(Dispatchers.IO) { val bytes = @@ -156,9 +91,9 @@ internal class JavaZipContainer( Try.success(bytes) } } catch (e: IOException) { - Try.failure(ResourceError.Filesystem(e)) + Try.failure(ReadError.Filesystem(FilesystemError.Unknown(e))) } catch (e: Exception) { - Try.failure(ResourceError.Other(e)) + Try.failure(ReadError.Other(e)) } private suspend fun readFully(): ByteArray = @@ -205,24 +140,19 @@ internal class JavaZipContainer( override val source: AbsoluteUrl = file.toUrl() - override suspend fun entries(): Set? = - tryOrLog { - archive.entries().toList() - .filterNot { it.isDirectory } - .mapNotNull { entry -> - Url.fromDecodedPath(entry.name) - ?.let { url -> Entry(url, entry) } - } - .toSet() - } + override suspend fun entries(): Set = + tryOrLog { archive.entries().toList() } + .orEmpty() + .filterNot { it.isDirectory } + .mapNotNull { entry -> Url.fromDecodedPath(entry.name) } + .toSet() - override fun get(url: Url): Container.Entry = + override fun get(url: Url): ResourceEntry? = (url as? RelativeUrl)?.path ?.let { tryOrLog { archive.getEntry(it) } } ?.let { Entry(url, it) } - ?: FailureEntry(url) override suspend fun close() { tryOrLog { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt index 5e8c4fb5eb..0118805f8d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt @@ -6,9 +6,10 @@ package org.readium.r2.shared.util.asset +import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Container as SharedContainer import org.readium.r2.shared.util.resource.Resource as SharedResource +import org.readium.r2.shared.util.resource.ResourceEntry /** * An asset which is either a single resource or a container that holds multiple resources. @@ -51,7 +52,7 @@ public sealed class Asset { public class Container( override val mediaType: MediaType, public val containerType: MediaType, - public val container: SharedContainer + public val container: ClosedContainer ) : Asset() { override suspend fun close() { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetError.kt deleted file mode 100644 index b9f5d4b660..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetError.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.asset - -import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.FilesystemError -import org.readium.r2.shared.util.NetworkError -import org.readium.r2.shared.util.ThrowableError - -/** - * Errors occurring while opening a Publication. - */ -public sealed class AssetError( - override val message: String, - override val cause: Error? = null -) : Error { - - /** - * The file format could not be recognized by any parser. - */ - public class UnsupportedAsset( - message: String, - cause: Error? - ) : AssetError(message, cause) { - public constructor(message: String) : this(message, null) - public constructor(cause: Error? = null) : this("Asset is not supported.", cause) - } - - /** - * The publication parsing failed with the given underlying error. - */ - public class InvalidAsset( - message: String, - cause: Error? = null - ) : AssetError(message, cause) { - public constructor(cause: Error?) : this( - "The asset seems corrupted so the publication cannot be opened.", - cause - ) - } - - /** - * The publication file was not found on the file system. - */ - public class NotFound(cause: Error? = null) : - AssetError("Asset could not be found.", cause) - - /** - * We're not allowed to open the publication at all, for example because it expired. - */ - public class Forbidden(cause: Error? = null) : - AssetError("You are not allowed to open this publication.", cause) - - public class Network(public override val cause: NetworkError) : - AssetError("A network error occurred.", cause) - - public class Filesystem(public override val cause: FilesystemError) : - AssetError("A filesystem error occurred.", cause) - - /** - * The provided credentials are incorrect and we can't open the publication in a - * `restricted` state (e.g. for a password-protected ZIP). - */ - public class IncorrectCredentials(cause: Error? = null) : - AssetError("Provided credentials were incorrect.", cause) - - /** - * Opening the publication exceeded the available device memory. - */ - public class OutOfMemory(cause: Error? = null) : - AssetError("There is not enough memory available to open the publication.", cause) - - /** - * An unexpected error occurred. - */ - public class Unknown(cause: Error? = null) : - AssetError("An unexpected error occurred.", cause) { - - public constructor(exception: Exception) : this(ThrowableError(exception)) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 2e8ebbfee5..3040be7c2d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -6,43 +6,24 @@ package org.readium.r2.shared.util.asset -import android.content.ContentResolver -import android.content.Context -import android.provider.MediaStore import java.io.File import kotlin.Exception import kotlin.String -import kotlin.let -import kotlin.takeUnless -import org.readium.r2.shared.extensions.queryProjection import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.Either import org.readium.r2.shared.util.Error as SharedError -import org.readium.r2.shared.util.FilesystemError -import org.readium.r2.shared.util.MessageError -import org.readium.r2.shared.util.NetworkError 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.archive.ArchiveFactory +import org.readium.r2.shared.util.archive.ArchiveProvider +import org.readium.r2.shared.util.archive.CompositeArchiveFactory +import org.readium.r2.shared.util.archive.FileZipArchiveProvider import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.CompositeMediaTypeSniffer 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 org.readium.r2.shared.util.mediatype.MediaTypeSniffer -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferContentError import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.resource.ArchiveFactory -import org.readium.r2.shared.util.resource.ArchiveProvider -import org.readium.r2.shared.util.resource.CompositeArchiveFactory -import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.ContainerMediaTypeSnifferContent -import org.readium.r2.shared.util.resource.FileResourceFactory -import org.readium.r2.shared.util.resource.FileZipArchiveProvider import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceError -import org.readium.r2.shared.util.resource.ResourceFactory -import org.readium.r2.shared.util.resource.ResourceMediaTypeSnifferContent import org.readium.r2.shared.util.toUrl /** @@ -50,10 +31,8 @@ import org.readium.r2.shared.util.toUrl * given [Url]. */ public class AssetRetriever( - private val mediaTypeRetriever: MediaTypeRetriever, - private val resourceFactory: ResourceFactory, - private val contentResolver: ContentResolver, - archiveProviders: List = listOf(FileZipArchiveProvider(mediaTypeRetriever)) + private val resourceFactory: ResourceFactory = FileResourceFactory(), + archiveProviders: List = listOf(FileZipArchiveProvider()) ) { private val archiveSniffer: MediaTypeSniffer = CompositeMediaTypeSniffer(archiveProviders) @@ -61,18 +40,6 @@ public class AssetRetriever( private val archiveFactory: ArchiveFactory = CompositeArchiveFactory(archiveProviders) - public companion object { - public operator fun invoke(context: Context): AssetRetriever { - val mediaTypeRetriever = MediaTypeRetriever() - return AssetRetriever( - mediaTypeRetriever = mediaTypeRetriever, - resourceFactory = FileResourceFactory(mediaTypeRetriever), - archiveProviders = emptyList(), - contentResolver = context.contentResolver - ) - } - } - public sealed class Error( override val message: String, override val cause: SharedError? @@ -87,22 +54,6 @@ public class AssetRetriever( this(scheme, ThrowableError(exception)) } - public class NotFound( - public val url: AbsoluteUrl, - cause: SharedError? - ) : Error("Asset could not be found at $url.", cause) { - - public constructor(url: AbsoluteUrl, exception: Exception) : - this(url, ThrowableError(exception)) - } - - public class InvalidAsset(cause: SharedError?) : - Error("Asset looks corrupted.", cause) { - - public constructor(exception: Exception) : - this(ThrowableError(exception)) - } - public class ArchiveFormatNotSupported(cause: SharedError?) : Error("Archive factory does not support this kind of archive.", cause) { @@ -110,29 +61,8 @@ public class AssetRetriever( this(ThrowableError(exception)) } - public class Forbidden( - public val url: AbsoluteUrl, - cause: SharedError? - ) : Error("Access to asset at url $url is forbidden.", cause) { - - public constructor(url: AbsoluteUrl, exception: Exception) : - this(url, ThrowableError(exception)) - } - - public class Network(public override val cause: NetworkError) : - Error("A network error occurred.", cause) - - public class Filesystem(public override val cause: FilesystemError) : - Error("A filesystem error occurred.", cause) - - public class OutOfMemory(error: OutOfMemoryError) : - Error( - "There is not enough memory on the device to load the asset.", - ThrowableError(error) - ) - - public class Unknown(error: SharedError) : - Error("Something unexpected happened.", error) + public class AccessError(override val cause: org.readium.r2.shared.util.data.ReadError) : + Error("An error occurred when trying to read asset.", cause) } /** @@ -160,16 +90,23 @@ public class AssetRetriever( val resource = retrieveResource(url, containerType) .getOrElse { return Try.failure(it) } - return retrieveArchiveAsset(url, resource, mediaType, containerType) + return retrieveArchiveAsset(resource, mediaType, containerType) } private suspend fun retrieveArchiveAsset( - url: AbsoluteUrl, resource: Resource, mediaType: MediaType, containerType: MediaType ): Try { val container = archiveFactory.create(resource) - .getOrElse { error -> return Try.failure(error.toAssetRetrieverError(url)) } + .mapFailure { error -> + when (error) { + is ArchiveFactory.Error.ResourceError -> + Error.AccessError(error.cause) + else -> + Error.ArchiveFormatNotSupported(error) + } + } + .getOrElse { return Try.failure(it) } val asset = Asset.Container( mediaType = mediaType, @@ -180,18 +117,6 @@ public class AssetRetriever( return Try.success(asset) } - private fun ArchiveFactory.Error.toAssetRetrieverError(url: AbsoluteUrl): Error = - when (this) { - is ArchiveFactory.Error.UnsupportedFormat -> - Error.ArchiveFormatNotSupported(this) - - is ArchiveFactory.Error.ResourceError -> - cause.wrap(url) - - is ArchiveFactory.Error.PasswordsNotSupported -> - Error.ArchiveFormatNotSupported(this) - } - private suspend fun retrieveResourceAsset( url: AbsoluteUrl, mediaType: MediaType @@ -218,30 +143,6 @@ public class AssetRetriever( } } - private fun ResourceError.wrap(url: AbsoluteUrl): Error = - when (this) { - is ResourceError.Forbidden -> - Error.Forbidden(url, this) - - is ResourceError.NotFound -> - Error.InvalidAsset(this) - - is ResourceError.Network -> - Error.Network(cause) - - is ResourceError.OutOfMemory -> - Error.OutOfMemory(cause.throwable) - - is ResourceError.Other -> - Error.Unknown(this) - - is ResourceError.InvalidContent -> - Error.InvalidAsset(this) - - is ResourceError.Filesystem -> - Error.Filesystem(cause) - } - /* Sniff unknown assets */ /** @@ -264,10 +165,10 @@ public class AssetRetriever( ) } - val mediaType = retrieveMediaType(url, Either.Left(resource)) - .getOrElse { return Try.failure(it.wrap(url)) } + val mediaType = resource.mediaType() + .getOrElse { return Try.failure(Error.AccessError(it)) } - return archiveSniffer.sniffResource(ResourceMediaTypeSnifferContent(resource)) + return archiveSniffer.sniffBlob(resource) .fold( { containerType -> retrieveArchiveAsset(url, mediaType = mediaType, containerType = containerType) @@ -276,76 +177,10 @@ public class AssetRetriever( when (error) { MediaTypeSnifferError.NotRecognized -> Try.success(Asset.Resource(mediaType, resource)) - is MediaTypeSnifferError.SourceError -> - Try.failure(error.wrap(url)) + is MediaTypeSnifferError.DataAccess -> + Try.failure(Error.AccessError(error.cause)) } } ) } - - private fun MediaTypeSnifferError.wrap(url: AbsoluteUrl) = when (this) { - is MediaTypeSnifferError.SourceError -> - when (cause) { - is MediaTypeSnifferContentError.Filesystem -> - Error.Filesystem(cause.cause) - is MediaTypeSnifferContentError.Forbidden -> - Error.Forbidden(url, cause.cause) - is MediaTypeSnifferContentError.Network -> - Error.Network(cause.cause) - is MediaTypeSnifferContentError.NotFound -> - Error.NotFound(url, cause.cause) - is MediaTypeSnifferContentError.ArchiveError -> - Error.InvalidAsset(cause) - is MediaTypeSnifferContentError.TooBig -> - Error.OutOfMemory(cause.cause.throwable) - is MediaTypeSnifferContentError.Unknown -> - Error.Unknown(cause) - } - MediaTypeSnifferError.NotRecognized -> - Error.Unknown(MessageError("Cannot determine media type.")) - } - - private suspend fun retrieveMediaType( - url: AbsoluteUrl, - asset: Either - ): Try { - suspend fun retrieve(hints: MediaTypeHints): Try = - mediaTypeRetriever.retrieve( - hints = hints, - content = when (asset) { - is Either.Left -> ResourceMediaTypeSnifferContent(asset.value) - is Either.Right -> ContainerMediaTypeSnifferContent(asset.value) - } - ) - - retrieve(MediaTypeHints(fileExtensions = listOfNotNull(url.extension))) - .onSuccess { return Try.success(it) } - .onFailure { error -> - if (error is MediaTypeSnifferError.SourceError) { - return Try.failure(error) - } - } - - // Falls back on the [contentResolver] in case of content Uri. - // Note: This is done after the heavy sniffing of the provided [sniffers], because - // otherwise it will detect JSON, XML or ZIP formats before we have a chance of sniffing - // their content (for example, for RWPM). - - if (url.isContent) { - val contentHints = MediaTypeHints( - mediaType = contentResolver.getType(url.uri) - ?.let { MediaType(it) } - ?.takeUnless { it.matches(MediaType.BINARY) }, - fileExtension = contentResolver - .queryProjection(url.uri, MediaStore.MediaColumns.DISPLAY_NAME) - ?.let { filename -> File(filename).extension } - ) - - retrieve(contentHints) - .getOrNull() - ?.let { return Try.success(it) } - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt new file mode 100644 index 0000000000..4374360bd4 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.asset + +import android.content.ContentResolver +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ContentBlob +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 org.readium.r2.shared.util.resource.GuessMediaTypeResourceAdapter +import org.readium.r2.shared.util.resource.KnownMediaTypeResourceAdapter +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.toUri + +/** + * Creates [ContentBlob]s. + */ +public class ContentResourceFactory( + private val contentResolver: ContentResolver, + private val mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(contentResolver) +) : ResourceFactory { + + override suspend fun create( + url: AbsoluteUrl, + mediaType: MediaType? + ): Try { + if (!url.isContent) { + return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) + } + + val blob = ContentBlob(url.toUri(), contentResolver) + + val resource = mediaType + ?.let { KnownMediaTypeResourceAdapter(blob, it) } + ?: GuessMediaTypeResourceAdapter( + blob, + mediaTypeRetriever, + MediaTypeHints(fileExtension = url.extension) + ) + + return Try.success(resource) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt new file mode 100644 index 0000000000..8d99a84773 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.asset + +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.FileBlob +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 org.readium.r2.shared.util.resource.GuessMediaTypeResourceAdapter +import org.readium.r2.shared.util.resource.KnownMediaTypeResourceAdapter +import org.readium.r2.shared.util.resource.Resource + +public class FileResourceFactory( + private val mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever() +) : ResourceFactory { + + override suspend fun create( + url: AbsoluteUrl, + mediaType: MediaType? + ): Try { + val file = url.toFile() + ?: return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) + + val blob = FileBlob(file) + + val resource = mediaType + ?.let { KnownMediaTypeResourceAdapter(blob, it) } + ?: GuessMediaTypeResourceAdapter( + blob, + mediaTypeRetriever, + MediaTypeHints(fileExtension = file.extension) + ) + + return Try.success(resource) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt similarity index 85% rename from readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt index a78476f9a9..50070712e5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt @@ -4,13 +4,14 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.http +package org.readium.r2.shared.util.asset import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpResource import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceFactory public class HttpResourceFactory( private val httpClient: HttpClient diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt similarity index 94% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt index c542b2c9c6..2a829d8bd8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.asset import kotlin.String import kotlin.let @@ -13,6 +13,7 @@ import org.readium.r2.shared.util.Error as SharedError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource /** * A factory to read [Resource]s from [Url]s. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Blob.kt similarity index 68% rename from readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSource.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/Blob.kt index b8c838f946..73503d84da 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Blob.kt @@ -4,15 +4,22 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.datasource +package org.readium.r2.shared.util.data +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Try /** * Acts as a proxy to an actual data source by handling read access. */ -internal interface DataSource : SuspendingCloseable { +public interface Blob : SuspendingCloseable { + + /** + * URL locating this resource, if any. + */ + public val source: AbsoluteUrl? /** * Returns data length from metadata if available, or calculated from reading the bytes otherwise. @@ -20,7 +27,7 @@ internal interface DataSource : SuspendingCloseable { * This value must be treated as a hint, as it might not reflect the actual bytes length. To get * the real length, you need to read the whole resource. */ - suspend fun length(): Try + public suspend fun length(): Try /** * Reads the bytes at the given range. @@ -28,5 +35,5 @@ internal interface DataSource : SuspendingCloseable { * When [range] is null, the whole content is returned. Out-of-range indexes are clamped to the * available length automatically. */ - suspend fun read(range: LongRange? = null): Try + public suspend fun read(range: LongRange? = null): Try } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSourceInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt similarity index 77% rename from readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSourceInputStream.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt index e159bd9d06..135cfd7ca6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSourceInputStream.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt @@ -4,22 +4,22 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.datasource +package org.readium.r2.shared.util.data import java.io.IOException import java.io.InputStream import kotlinx.coroutines.runBlocking import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.getOrThrow +import org.readium.r2.shared.util.Try /** - * Input stream reading through a [DataSource]. + * Input stream reading through a [Blob]. * * If you experience bad performances, consider wrapping the stream in a BufferedInputStream. This * is particularly useful when streaming deflated ZIP entries. */ -internal class DataSourceInputStream( - private val dataSource: DataSource, +public class BlobInputStream( + private val blob: Blob, private val wrapError: (E) -> IOException, private val range: LongRange? = null ) : InputStream() { @@ -28,9 +28,8 @@ internal class DataSourceInputStream( private val end: Long by lazy { val resourceLength = - runBlocking { dataSource.length() } - .mapFailure { wrapError(it) } - .getOrThrow() + runBlocking { blob.length() } + .recover() if (range == null) { resourceLength @@ -47,6 +46,14 @@ internal class DataSourceInputStream( */ private var mark: Long = range?.start ?: 0 + private var error: E? = null + + internal fun consumeError(): E? { + val errorNow = error + error = null + return errorNow + } + override fun available(): Int { checkNotClosed() return (end - position).toInt() @@ -69,9 +76,8 @@ internal class DataSourceInputStream( } val bytes = runBlocking { - dataSource.read(position until (position + 1)) - .mapFailure { wrapError(it) } - .getOrThrow() + blob.read(position until (position + 1)) + .recover() } position += 1 return bytes.first().toUByte().toInt() @@ -86,9 +92,8 @@ internal class DataSourceInputStream( val bytesToRead = len.coerceAtMost(available()) val bytes = runBlocking { - dataSource.read(position until (position + bytesToRead)) - .mapFailure { wrapError(it) } - .getOrThrow() + blob.read(position until (position + bytesToRead)) + .recover() } check(bytes.size <= bytesToRead) bytes.copyInto( @@ -135,4 +140,15 @@ internal class DataSourceInputStream( throw IllegalStateException("InputStream is closed.") } } + + private fun Try.recover(): S = + when (this) { + is Try.Success -> { + value + } + is Try.Failure -> { + error = value + throw wrapError(value) + } + } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt new file mode 100644 index 0000000000..7c4389c83e --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.SuspendingCloseable +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.resource.Resource + +/** + * Represents a container entry's. + */ +public interface ContainerEntry : Blob { + + /** + * URL used to access the resource in the container. + */ + public val url: Url +} + +/** + * A container provides access to a list of [Resource] entries. + */ +public interface Container : SuspendingCloseable { + + /** + * Direct source to this container, when available. + */ + public val source: AbsoluteUrl? get() = null + + /** + * Returns the [Entry] at the given [url]. + * + * A [Entry] is always returned, since for some cases we can't know if it exists before actually + * fetching it, such as HTTP. Therefore, errors are handled at the Entry level. + */ + public fun get(url: Url): E? +} + +public interface ClosedContainer : Container { + + /** + * List of all the container entries. + */ + public suspend fun entries(): Set +} + +/** A [Container] providing no resources at all. */ +public class EmptyContainer : ClosedContainer { + + override suspend fun entries(): Set = emptySet() + + override fun get(url: Url): E? = null + + override suspend fun close() {} +} + +/** + * Returns whether an entry exists in the container. + */ +internal suspend fun Container.contains(url: Url): Try { + if (this is ClosedContainer) { + return Try.success(url in entries()) + } + + return get(url) + ?.read(range = 0L..1L) + ?.map { true } + ?: Try.success(false) +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt similarity index 58% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt index 673b832add..9d545aa079 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.data import android.content.ContentResolver import android.net.Uri @@ -16,59 +16,26 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.FilesystemError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.toUri import org.readium.r2.shared.util.toUrl -/** - * Creates [ContentResource]s. - */ -public class ContentResourceFactory( - private val contentResolver: ContentResolver -) : ResourceFactory { - - override suspend fun create( - url: AbsoluteUrl, - mediaType: MediaType? - ): Try { - if (!url.isContent) { - return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) - } - - val resource = ContentResource(url.toUri(), contentResolver, mediaType) - - return Try.success(resource) - } -} - /** * A [Resource] to access content [uri] thanks to a [ContentResolver]. */ -public class ContentResource internal constructor( +public class ContentBlob( private val uri: Uri, - private val contentResolver: ContentResolver, - private val mediaType: MediaType? = null -) : Resource { + private val contentResolver: ContentResolver +) : Blob { - private lateinit var _length: ResourceTry + private lateinit var _length: Try override val source: AbsoluteUrl? = uri.toUrl() as? AbsoluteUrl - override suspend fun properties(): ResourceTry = - ResourceTry.success(Resource.Properties()) - - override suspend fun mediaType(): ResourceTry = - Try.success( - mediaType - ?: contentResolver.getType(uri)?.let { MediaType(it) } - ?: MediaType.BINARY - ) - override suspend fun close() { } - override suspend fun read(range: LongRange?): ResourceTry { + override suspend fun read(range: LongRange?): Try { if (range == null) { return readFully() } @@ -85,10 +52,10 @@ public class ContentResource internal constructor( return readRange(range) } - private suspend fun readFully(): ResourceTry = + private suspend fun readFully(): Try = withStream { it.readFully() } - private suspend fun readRange(range: LongRange): ResourceTry = + private suspend fun readRange(range: LongRange): Try = withStream { withContext(Dispatchers.IO) { val skipped = it.skip(range.first) @@ -98,9 +65,9 @@ public class ContentResource internal constructor( } } - override suspend fun length(): ResourceTry { + override suspend fun length(): Try { if (!::_length.isInitialized) { - _length = ResourceTry.catching { + _length = Try.catching { contentResolver.openFileDescriptor(uri, "r") .use { fd -> checkNotNull(fd?.statSize.takeUnless { it == -1L }) } } @@ -109,11 +76,11 @@ public class ContentResource internal constructor( return _length } - private suspend fun withStream(block: suspend (InputStream) -> T): Try { - return ResourceTry.catching { + private suspend fun withStream(block: suspend (InputStream) -> T): Try { + return Try.catching { val stream = contentResolver.openInputStream(uri) ?: return Try.failure( - ResourceError.Other( + ReadError.Other( Exception("Content provider recently crashed.") ) ) @@ -123,19 +90,19 @@ public class ContentResource internal constructor( } } - private inline fun Try.Companion.catching(closure: () -> T): ResourceTry = + private inline fun Try.Companion.catching(closure: () -> T): Try = try { success(closure()) } catch (e: FileNotFoundException) { - failure(ResourceError.NotFound(e)) + failure(ReadError.Filesystem(FilesystemError.NotFound(e))) } catch (e: SecurityException) { - failure(ResourceError.Forbidden(e)) + failure(ReadError.Filesystem(FilesystemError.Forbidden(e))) } catch (e: IOException) { - failure(ResourceError.Filesystem(e)) + failure(ReadError.Filesystem(FilesystemError.Unknown(e))) } catch (e: Exception) { - failure(ResourceError.Other(e)) + failure(ReadError.Filesystem(FilesystemError.Unknown(e))) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - failure(ResourceError.OutOfMemory(e)) + failure(ReadError.OutOfMemory(e)) } override fun toString(): String = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSourceDecoder.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt similarity index 75% rename from readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSourceDecoder.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index ae98b78a3b..613a767c73 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/datasource/DataSourceDecoder.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.datasource +package org.readium.r2.shared.util.data import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -19,18 +19,19 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.xml.ElementNode import org.readium.r2.shared.util.xml.XmlParser -internal sealed class DecoderError( +public sealed class DecoderError( override val message: String ) : Error { - class DataSourceError( + public class DataAccess( override val cause: E ) : DecoderError("Data source error") - class DecodingError( + public class DecodingError( override val cause: Error? ) : DecoderError("Decoding Error") } @@ -51,7 +52,7 @@ internal suspend fun Try.decode( Try.failure(DecoderError.DecodingError(wrapException(e))) } is Try.Failure -> - Try.failure(DecoderError.DataSourceError(value)) + Try.failure(DecoderError.DataAccess(value)) } internal suspend fun Try>.decodeMap( @@ -79,7 +80,7 @@ internal suspend fun Try>.decodeMap( * It will extract the charset parameter from the media type hints to figure out an encoding. * Otherwise, fallback on UTF-8. */ -internal suspend fun DataSource.readAsString( +public suspend fun Blob.readAsString( charset: Charset = Charsets.UTF_8 ): Try> = read().decode( @@ -88,7 +89,7 @@ internal suspend fun DataSource.readAsString( ) /** Content as an XML document. */ -internal suspend fun DataSource.readAsXml(): Try> = +public suspend fun Blob.readAsXml(): Try> = read().decode( { XmlParser().parse(ByteArrayInputStream(it)) }, { MessageError("Content is not a valid XML document.", ThrowableError(it)) } @@ -97,20 +98,20 @@ internal suspend fun DataSource.readAsXml(): Try DataSource.readAsJson(): Try> = +public suspend fun Blob.readAsJson(): Try> = readAsString().decodeMap( { JSONObject(it) }, { MessageError("Content is not valid JSON.", ThrowableError(it)) } ) /** Readium Web Publication Manifest parsed from the content. */ -internal suspend fun DataSource.readAsRwpm(): Try> = +public suspend fun Blob.readAsRwpm(): Try> = readAsJson().flatMap { json -> Manifest.fromJSON(json) ?.let { Try.success(it) } ?: Try.failure( DecoderError.DecodingError( - MessageError("Content is not a valid RPWM.") + MessageError("Content is not a valid RWPM.") ) ) } @@ -118,9 +119,9 @@ internal suspend fun DataSource.readAsRwpm(): Try DataSource.readAsBitmap(): Try> = +public suspend fun Blob.readAsBitmap(): Try> = read() - .mapFailure { DecoderError.DataSourceError(it) } + .mapFailure { DecoderError.DataAccess(it) } .flatMap { bytes -> BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?.let { Try.success(it) } @@ -130,3 +131,14 @@ internal suspend fun DataSource.readAsBitmap(): Try Blob.containsJsonKeys( + vararg keys: String +): Try> { + val json = readAsJson() + .getOrElse { return Try.failure(it) } + return Try.success(json.keys().asSequence().toSet().containsAll(keys.toList())) +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt similarity index 57% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt index f669b83373..47a6a12565 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.data import java.io.File import java.io.FileNotFoundException @@ -15,28 +15,18 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.FilesystemError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.isLazyInitialized -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 org.readium.r2.shared.util.toUrl /** * A [Resource] to access a [file]. */ -public class FileResource private constructor( - private val file: File, - private val mediaType: MediaType?, - private val mediaTypeRetriever: MediaTypeRetriever? -) : Resource { - - public constructor(file: File, mediaType: MediaType) : - this(file, mediaType, null) - - public constructor(file: File, mediaTypeRetriever: MediaTypeRetriever) : - this(file, null, mediaTypeRetriever) +public class FileBlob( + private val file: File +) : Blob { private val randomAccessFile by lazy { try { @@ -48,17 +38,6 @@ public class FileResource private constructor( override val source: AbsoluteUrl = file.toUrl() - override suspend fun properties(): ResourceTry = - ResourceTry.success(Resource.Properties()) - - override suspend fun mediaType(): ResourceTry = - mediaType - ?.let { Try.success(it) } - ?: mediaTypeRetriever!!.retrieve( - hints = MediaTypeHints(fileExtension = file.extension), - content = ResourceMediaTypeSnifferContent(this) - ).toResourceTry() - override suspend fun close() { withContext(Dispatchers.IO) { if (::randomAccessFile.isLazyInitialized) { @@ -69,9 +48,9 @@ public class FileResource private constructor( } } - override suspend fun read(range: LongRange?): ResourceTry = + override suspend fun read(range: LongRange?): Try = withContext(Dispatchers.IO) { - ResourceTry.catching { + Try.catching { readSync(range) } } @@ -102,7 +81,7 @@ public class FileResource private constructor( } } - override suspend fun length(): ResourceTry = + override suspend fun length(): Try = metadataLength?.let { Try.success(it) } ?: read().map { it.size.toLong() } @@ -115,40 +94,21 @@ public class FileResource private constructor( } } - private inline fun Try.Companion.catching(closure: () -> T): ResourceTry = + private inline fun Try.Companion.catching(closure: () -> T): Try = try { success(closure()) } catch (e: FileNotFoundException) { - failure(ResourceError.NotFound(e)) + failure(ReadError.Filesystem(FilesystemError.NotFound(e))) } catch (e: SecurityException) { - failure(ResourceError.Forbidden(e)) + failure(ReadError.Filesystem(FilesystemError.Forbidden(e))) } catch (e: IOException) { - failure(ResourceError.Filesystem(e)) + failure(ReadError.Filesystem(FilesystemError.Unknown(e))) } catch (e: Exception) { - failure(ResourceError.Other(e)) + failure(ReadError.Filesystem(FilesystemError.Unknown(e))) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - failure(ResourceError.OutOfMemory(e)) + failure(ReadError.OutOfMemory(e)) } override fun toString(): String = "${javaClass.simpleName}(${file.path})" } - -public class FileResourceFactory( - private val mediaTypeRetriever: MediaTypeRetriever -) : ResourceFactory { - - override suspend fun create( - url: AbsoluteUrl, - mediaType: MediaType? - ): Try { - val file = url.toFile() - ?: return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) - - val resource = mediaType - ?.let { FileResource(file, mediaType) } - ?: FileResource(file, mediaTypeRetriever) - - return Try.success(resource) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt new file mode 100644 index 0000000000..2e73f53262 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import java.io.IOException +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.FilesystemError +import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.ThrowableError + +/** + * Errors occurring while accessing a resource. + */ +public sealed class ReadError( + override val message: String, + override val cause: Error? = null +) : Error { + + public class Network(public override val cause: Error) : + ReadError("A network error occurred.", cause) + + public class Filesystem(public override val cause: FilesystemError) : + ReadError("A filesystem error occurred.", cause) + + /** + * Equivalent to a 507 HTTP error. + * + * Used when the requested range is too large to be read in memory. + */ + public class OutOfMemory(override val cause: ThrowableError) : + ReadError("The resource is too large to be read on this device.", cause) { + + public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) + } + + public class Content(cause: Error? = null) : + ReadError("Content seems invalid. ", cause) { + + public constructor(message: String) : this(MessageError(message)) + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + /** For any other error, such as HTTP 500. */ + public class Other(cause: Error) : + ReadError("An unclassified error occurred.", cause) { + + public constructor(message: String) : this(MessageError(message)) + public constructor(exception: Exception) : this(ThrowableError(exception)) + } +} + +public class AccessException( + public val error: ReadError +) : IOException(error.message, ErrorException(error)) + +internal fun Exception.unwrapAccessException(): Exception { + fun Throwable.findResourceExceptionCause(): AccessException? = + when { + this is AccessException -> this + cause != null -> cause!!.findResourceExceptionCause() + else -> null + } + + this.findResourceExceptionCause()?.let { return it } + return this +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingClosedContainer.kt similarity index 71% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingClosedContainer.kt index f7477a8bb3..fbcca7b271 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/RoutingContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingClosedContainer.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.data import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url @@ -17,19 +17,21 @@ import org.readium.r2.shared.util.Url * * The [routes] will be tested in the given order. */ -public class RoutingContainer(private val routes: List) : Container { +public class RoutingClosedContainer( + private val routes: List> +) : ClosedContainer { /** * Holds a child fetcher and the predicate used to determine if it can answer a request. * * The default value for [accepts] means that the fetcher will accept any link. */ - public class Route( - public val container: Container, + public class Route( + public val container: ClosedContainer, public val accepts: (Url) -> Boolean = { true } ) - public constructor(local: Container, remote: Container) : + public constructor(local: ClosedContainer, remote: ClosedContainer) : this( listOf( Route(local, accepts = ::isLocal), @@ -37,12 +39,11 @@ public class RoutingContainer(private val routes: List) : Container { ) ) - override suspend fun entries(): Set? = - null // We can't guarantee the list of entries is exhaustive, so we return null + override suspend fun entries(): Set = + routes.fold(emptySet()) { acc, route -> acc + route.container.entries() } - override fun get(url: Url): Container.Entry = + override fun get(url: Url): E? = routes.firstOrNull { it.accepts(url) }?.container?.get(url) - ?: FailureResource(ResourceError.NotFound()).toEntry(url) override suspend fun close() { routes.forEach { it.container.close() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index d642590d09..34ef13697a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -26,12 +26,14 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.tryOr import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpError 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 -import org.readium.r2.shared.util.resource.FileResource import org.readium.r2.shared.util.toUri import org.readium.r2.shared.util.units.Hz import org.readium.r2.shared.util.units.hz @@ -272,9 +274,14 @@ public class AndroidDownloadManager internal constructor( private suspend fun prepareResult(destFile: File, mediaTypeHint: String?): Try = withContext(Dispatchers.IO) { - val mediaType = mediaTypeHint?.let { mediaTypeRetriever.retrieve(it) } - ?: FileResource(destFile, mediaTypeRetriever).mediaType().getOrNull() - ?: MediaType.BINARY + val mediaType = mediaTypeRetriever.retrieve( + hints = MediaTypeHints( + mediaTypes = listOfNotNull( + mediaTypeHint?.let { MediaType(it) } + ) + ), + blob = FileBlob(destFile) + ).getOrElse { MediaType.BINARY } val extension = formatRegistry.fileExtension(mediaType) ?: destFile.extension.takeUnless { it.isEmpty() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index 55ad42a978..97b634db0b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -26,10 +26,10 @@ import org.readium.r2.shared.util.e import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrDefault import org.readium.r2.shared.util.http.HttpRequest.Method -import org.readium.r2.shared.util.mediatype.BytesResourceMediaTypeSnifferContent 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 org.readium.r2.shared.util.resource.BytesResource import org.readium.r2.shared.util.tryRecover import timber.log.Timber @@ -172,7 +172,7 @@ public class DefaultHttpClient( val mediaType = body?.let { mediaTypeRetriever.retrieve( hints = MediaTypeHints(connection), - content = BytesResourceMediaTypeSnifferContent { it } + blob = BytesResource { it } ).getOrDefault(MediaType.BINARY) } return@withContext Try.failure(HttpError(kind, mediaType, body)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt index b51c48c0a8..0c31423505 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt @@ -8,15 +8,14 @@ package org.readium.r2.shared.util.http import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.FailureResource -import org.readium.r2.shared.util.resource.ResourceError -import org.readium.r2.shared.util.resource.toEntry +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.resource.ResourceEntry +import org.readium.r2.shared.util.resource.toResourceEntry /** * Fetches remote resources through HTTP. * - * Since this fetcher is used when doing progressive download streaming (e.g. audiobook), the HTTP + * Since this container is used when doing progressive download streaming (e.g. audiobook), the HTTP * byte range requests are open-ended and reused. This helps to avoid issuing too many requests. * * @param client HTTP client used to perform HTTP requests. @@ -24,24 +23,20 @@ import org.readium.r2.shared.util.resource.toEntry */ public class HttpContainer( private val client: HttpClient, - private val baseUrl: Url? = null -) : Container { + private val baseUrl: Url? = null, + private val entries: Set +) : ClosedContainer { - override suspend fun entries(): Set? = null + override suspend fun entries(): Set = entries - override fun get(url: Url): Container.Entry { + override fun get(url: Url): ResourceEntry? { val absoluteUrl = (baseUrl?.resolve(url) ?: url) as? AbsoluteUrl return if (absoluteUrl == null || !absoluteUrl.isHttp) { - FailureResource( - ResourceError.NotFound( - Exception("URL scheme is not supported: ${absoluteUrl?.scheme}.") - ) - ) + null } else { - HttpResource(client, absoluteUrl) + HttpResource(client, absoluteUrl).toResourceEntry(url) } - .toEntry(url) } override suspend fun close() {} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index dbdf75b53b..64579723a2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -10,12 +10,11 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.NetworkError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.io.CountingInputStream import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceError -import org.readium.r2.shared.util.resource.ResourceTry /** Provides access to an external URL. */ @OptIn(ExperimentalReadiumApi::class) @@ -25,25 +24,25 @@ public class HttpResource( private val maxSkipBytes: Long = MAX_SKIP_BYTES ) : Resource { - override suspend fun mediaType(): ResourceTry = + override suspend fun mediaType(): Try = headResponse().map { it.mediaType } - override suspend fun properties(): ResourceTry = - ResourceTry.success(Resource.Properties()) + override suspend fun properties(): Try = + Try.success(Resource.Properties()) - override suspend fun length(): ResourceTry = + override suspend fun length(): Try = headResponse().flatMap { val contentLength = it.contentLength return if (contentLength != null) { Try.success(contentLength) } else { - Try.failure(ResourceError.Other(UnsupportedOperationException())) + Try.failure(ReadError.Other(UnsupportedOperationException())) } } override suspend fun close() {} - override suspend fun read(range: LongRange?): ResourceTry = withContext( + override suspend fun read(range: LongRange?): Try = withContext( Dispatchers.IO ) { try { @@ -55,20 +54,20 @@ public class HttpResource( } } } catch (e: Exception) { - Try.failure(ResourceError.Other(e)) + Try.failure(ReadError.Other(e)) } } /** Cached HEAD response to get the expected content length and other metadata. */ - private lateinit var _headResponse: ResourceTry + private lateinit var _headResponse: Try - private suspend fun headResponse(): ResourceTry { + private suspend fun headResponse(): Try { if (::_headResponse.isInitialized) { return _headResponse } _headResponse = client.head(HttpRequest(source.toString())) - .mapFailure { ResourceError.wrapHttp(it) } + .mapFailure { it.wrap() } return _headResponse } @@ -79,7 +78,7 @@ public class HttpResource( * The stream is cached and reused for next calls, if the next [from] offset is not too far * and in a forward direction. */ - private suspend fun stream(from: Long? = null): ResourceTry { + private suspend fun stream(from: Long? = null): Try { val stream = inputStream if (from != null && stream != null) { tryOrLog { @@ -107,7 +106,7 @@ public class HttpResource( } } .map { CountingInputStream(it.body) } - .mapFailure { ResourceError.wrapHttp(it) } + .mapFailure { it.wrap() } .onSuccess { inputStream = it inputStreamStart = from ?: 0 @@ -117,20 +116,20 @@ public class HttpResource( private var inputStream: CountingInputStream? = null private var inputStreamStart = 0L - private fun ResourceError.Companion.wrapHttp(e: HttpError): ResourceError = - when (e.kind) { + private fun HttpError.wrap(): ReadError = + when (this.kind) { HttpError.Kind.MalformedRequest, HttpError.Kind.BadRequest, HttpError.Kind.MethodNotAllowed -> - ResourceError.Network(NetworkError.BadRequest(cause = e)) + ReadError.Network(NetworkError.BadRequest(cause = this)) HttpError.Kind.Timeout, HttpError.Kind.Offline -> - ResourceError.Network(NetworkError.Offline(e)) + ReadError.Network(NetworkError.Offline(this)) HttpError.Kind.Unauthorized, HttpError.Kind.Forbidden -> - ResourceError.Forbidden(e) + ReadError.Network(NetworkError.Forbidden(this)) HttpError.Kind.NotFound -> - ResourceError.NotFound(e) + ReadError.Network(NetworkError.NotFound(this)) HttpError.Kind.Cancelled, HttpError.Kind.TooManyRedirects -> - ResourceError.Other(e) + ReadError.Other(this) HttpError.Kind.MalformedResponse, HttpError.Kind.ClientError, HttpError.Kind.ServerError, HttpError.Kind.Other -> - ResourceError.Other(e) + ReadError.Other(this) } public companion object { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt new file mode 100644 index 0000000000..5b3f689b31 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.mediatype + +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError + +/** + * The default composite sniffer provided by Readium for all known formats. + * The sniffers order is important, because some formats are subsets of other formats. + */ +public class DefaultMediaTypeSniffer : MediaTypeSniffer { + + private val sniffer: MediaTypeSniffer = + CompositeMediaTypeSniffer( + listOf( + XhtmlMediaTypeSniffer(), + HtmlMediaTypeSniffer(), + OpdsMediaTypeSniffer, + LcpLicenseMediaTypeSniffer, + BitmapMediaTypeSniffer, + WebPubManifestMediaTypeSniffer(), + WebPubMediaTypeSniffer(), + W3cWpubMediaTypeSniffer, + EpubMediaTypeSniffer(), + LpfMediaTypeSniffer, + ArchiveMediaTypeSniffer, + PdfMediaTypeSniffer, + JsonMediaTypeSniffer + ) + ) + + override fun sniffHints(hints: MediaTypeHints): Try = + sniffer.sniffHints(hints) + + override suspend fun sniffBlob(blob: Blob): Try = + sniffer.sniffBlob(blob) + + override suspend fun sniffContainer(container: Container<*>): Try = + sniffer.sniffContainer(container) +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt index 2930c159b5..a8ce3b5943 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt @@ -6,7 +6,16 @@ package org.readium.r2.shared.util.mediatype +import android.content.ContentResolver +import android.provider.MediaStore +import java.io.File +import org.readium.r2.shared.extensions.queryProjection import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.FileBlob +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.toUri /** * Retrieves a canonical [MediaType] for the provided media type and file extension hints and/or @@ -16,46 +25,26 @@ import org.readium.r2.shared.util.Try * formats supported with Readium by default. */ public class MediaTypeRetriever( - private val sniffers: List = defaultSniffers -) { - - public companion object { - /** - * The default sniffers provided by Readium 2 for all known formats. - * The sniffers order is important, because some formats are subsets of other formats. - */ - public val defaultSniffers: List = listOf( - XhtmlMediaTypeSniffer, - HtmlMediaTypeSniffer, - OpdsMediaTypeSniffer, - LcpLicenseMediaTypeSniffer, - BitmapMediaTypeSniffer, - WebPubManifestMediaTypeSniffer, - WebPubMediaTypeSniffer, - W3cWpubMediaTypeSniffer, - EpubMediaTypeSniffer, - LpfMediaTypeSniffer, - ArchiveMediaTypeSniffer, - PdfMediaTypeSniffer, - JsonMediaTypeSniffer - ) - } + private val contentResolver: ContentResolver? = null, + private val mediaTypeSniffer: MediaTypeSniffer = DefaultMediaTypeSniffer() +) : MediaTypeSniffer { + + private val systemMediaTypeSniffer: MediaTypeSniffer = + SystemMediaTypeSniffer() /** * Retrieves a canonical [MediaType] for the provided media type and file extension [hints]. */ public fun retrieve(hints: MediaTypeHints): MediaType? { - for (sniffer in sniffers) { - sniffer.sniffHints(hints) - .getOrNull() - ?.let { return it } - } + mediaTypeSniffer.sniffHints(hints) + .getOrNull() + ?.let { return it } // Falls back on the system-wide registered media types using MimeTypeMap. // Note: This is done after the default sniffers, because otherwise it will detect // JSON, XML or ZIP formats before we have a chance of sniffing their content (for example, // for RWPM). - SystemMediaTypeSniffer.sniffHints(hints) + systemMediaTypeSniffer.sniffHints(hints) .getOrNull() ?.let { return it } @@ -88,50 +77,101 @@ public class MediaTypeRetriever( ): MediaType? = retrieve(MediaTypeHints(mediaTypes = mediaTypes, fileExtensions = fileExtensions)) + public suspend fun retrieve( + hints: MediaTypeHints = MediaTypeHints(), + container: Container<*>? = null + ): Try { + mediaTypeSniffer.sniffHints(hints) + .getOrNull() + ?.let { return Try.success(it) } + + if (container != null) { + mediaTypeSniffer.sniffContainer(container) + .onSuccess { return Try.success(it) } + .onFailure { error -> + when (error) { + is MediaTypeSnifferError.NotRecognized -> {} + else -> return Try.failure(error) + } + } + } + + return hints.mediaTypes.firstOrNull() + ?.let { Try.success(it) } + ?: Try.failure(MediaTypeSnifferError.NotRecognized) + } + + public suspend fun retrieve(file: File): Try = + retrieve( + hints = MediaTypeHints(fileExtension = file.extension), + blob = FileBlob(file) + ) + /** * Retrieves a canonical [MediaType] for the provided media type and file extensions [hints] and - * asset [content]. + * asset [blob]. */ public suspend fun retrieve( hints: MediaTypeHints = MediaTypeHints(), - content: MediaTypeSnifferContent? = null + blob: Blob? = null ): Try { - for (sniffer in sniffers) { - sniffer.sniffHints(hints) - .getOrNull() - ?.let { return Try.success(it) } - } + mediaTypeSniffer.sniffHints(hints) + .getOrNull() + ?.let { return Try.success(it) } - if (content != null) { - for (sniffer in sniffers) { - sniffer.sniffContent(content) - .onSuccess { return Try.success(it) } - .onFailure { error -> - if (error is MediaTypeSnifferError.SourceError) { - return Try.failure(error) - } + if (blob != null) { + mediaTypeSniffer.sniffBlob(blob) + .onSuccess { return Try.success(it) } + .onFailure { error -> + when (error) { + is MediaTypeSnifferError.NotRecognized -> {} + else -> return Try.failure(error) } - } + } } // Falls back on the system-wide registered media types using MimeTypeMap. // Note: This is done after the default sniffers, because otherwise it will detect // JSON, XML or ZIP formats before we have a chance of sniffing their content (for example, // for RWPM). - SystemMediaTypeSniffer.sniffHints(hints) + systemMediaTypeSniffer.sniffHints(hints) .getOrNull() ?.let { return Try.success(it) } - if (content != null) { - SystemMediaTypeSniffer.sniffContent(content) + if (blob != null) { + systemMediaTypeSniffer.sniffBlob(blob) .onSuccess { return Try.success(it) } .onFailure { error -> - if (error is MediaTypeSnifferError.SourceError) { - return Try.failure(error) + when (error) { + is MediaTypeSnifferError.NotRecognized -> {} + else -> return Try.failure(error) } } } + // Falls back on the [contentResolver] in case of content Uri. + // Note: This is done after the heavy sniffing of the provided [sniffers], because + // otherwise it will detect JSON, XML or ZIP formats before we have a chance of sniffing + // their content (for example, for RWPM). + + if (contentResolver != null) { + blob?.source + ?.takeIf { it.isContent } + ?.let { url -> + val contentHints = MediaTypeHints( + mediaType = contentResolver.getType(url.toUri()) + ?.let { MediaType(it) } + ?.takeUnless { it.matches(MediaType.BINARY) }, + fileExtension = contentResolver + .queryProjection(url.uri, MediaStore.MediaColumns.DISPLAY_NAME) + ?.let { filename -> File(filename).extension } + ) + + retrieve(contentHints) + ?.let { return Try.success(it) } + } + } + return hints.mediaTypes.firstOrNull() ?.let { Try.success(it) } ?: Try.failure(MediaTypeSnifferError.NotRecognized) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index d2e350ada1..e327852cd2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -7,6 +7,7 @@ package org.readium.r2.shared.util.mediatype import android.webkit.MimeTypeMap +import java.io.IOException import java.net.URLConnection import java.util.Locale import kotlinx.coroutines.Dispatchers @@ -16,46 +17,59 @@ import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.Error as BaseError +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.datasource.DecoderError +import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.BlobInputStream +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.contains +import org.readium.r2.shared.util.data.containsJsonKeys +import org.readium.r2.shared.util.data.readAsJson +import org.readium.r2.shared.util.data.readAsRwpm +import org.readium.r2.shared.util.data.readAsString +import org.readium.r2.shared.util.data.readAsXml import org.readium.r2.shared.util.getOrElse public sealed class MediaTypeSnifferError( override val message: String, - override val cause: BaseError? -) : BaseError { + override val cause: Error? +) : Error { public data object NotRecognized : MediaTypeSnifferError("Media type of resource could not be inferred.", null) - public data class SourceError(override val cause: MediaTypeSnifferContentError) : + public data class DataAccess(override val cause: ReadError) : MediaTypeSnifferError("An error occurred while trying to read content.", cause) } - public interface HintMediaTypeSniffer { public fun sniffHints( hints: MediaTypeHints ): Try } -public interface ResourceMediaTypeSniffer { - public suspend fun sniffResource( - resource: ResourceMediaTypeSnifferContent +public interface BlobMediaTypeSniffer { + public suspend fun sniffBlob( + blob: Blob ): Try } public interface ContainerMediaTypeSniffer { public suspend fun sniffContainer( - container: ContainerMediaTypeSnifferContent + container: Container<*> ): Try } /** * Sniffs a [MediaType] from media type and file extension hints or asset content. */ -public interface MediaTypeSniffer : HintMediaTypeSniffer, ResourceMediaTypeSniffer, ContainerMediaTypeSniffer { +public interface MediaTypeSniffer : + HintMediaTypeSniffer, + BlobMediaTypeSniffer, + ContainerMediaTypeSniffer { /** * Sniffs a [MediaType] from media type and file extension hints. @@ -66,33 +80,23 @@ public interface MediaTypeSniffer : HintMediaTypeSniffer, ResourceMediaTypeSniff Try.failure(MediaTypeSnifferError.NotRecognized) /** - * Sniffs a [MediaType] from a [ResourceMediaTypeSnifferContent]. + * Sniffs a [MediaType] from a [Blob]. */ - public override suspend fun sniffResource( - resource: ResourceMediaTypeSnifferContent + public override suspend fun sniffBlob( + blob: Blob ): Try = Try.failure(MediaTypeSnifferError.NotRecognized) /** - * Sniffs a [MediaType] from a [ContainerMediaTypeSnifferContent]. + * Sniffs a [MediaType] from a [Container]. */ public override suspend fun sniffContainer( - container: ContainerMediaTypeSnifferContent + container: Container<*> ): Try = Try.failure(MediaTypeSnifferError.NotRecognized) } -internal suspend fun MediaTypeSniffer.sniffContent( - content: MediaTypeSnifferContent -): Try = - when (content) { - is ContainerMediaTypeSnifferContent -> - sniffContainer(content) - is ResourceMediaTypeSnifferContent -> - sniffResource(content) - } - -internal class CompositeMediaTypeSniffer( +internal open class CompositeMediaTypeSniffer( private val sniffers: List ) : MediaTypeSniffer { @@ -106,14 +110,14 @@ internal class CompositeMediaTypeSniffer( return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { + override suspend fun sniffBlob(blob: Blob): Try { for (sniffer in sniffers) { - sniffer.sniffResource(resource) + sniffer.sniffBlob(blob) .getOrElse { error -> when (error) { MediaTypeSnifferError.NotRecognized -> null - is MediaTypeSnifferError.SourceError -> + else -> return Try.failure(error) } } @@ -123,14 +127,14 @@ internal class CompositeMediaTypeSniffer( return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContainer(container: ContainerMediaTypeSnifferContent): Try { + override suspend fun sniffContainer(container: Container<*>): Try { for (sniffer in sniffers) { sniffer.sniffContainer(container) .getOrElse { error -> when (error) { MediaTypeSnifferError.NotRecognized -> null - is MediaTypeSnifferError.SourceError -> + else -> return Try.failure(error) } } @@ -146,7 +150,7 @@ internal class CompositeMediaTypeSniffer( * * Must precede the HTML sniffer. */ -public object XhtmlMediaTypeSniffer : MediaTypeSniffer { +public class XhtmlMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("xht", "xhtml") || @@ -158,12 +162,14 @@ public object XhtmlMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { - resource.contentAsXml() + override suspend fun sniffBlob(blob: Blob): Try { + blob.readAsXml() .getOrElse { when (it) { - is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) + is DecoderError.DataAccess -> + return Try.failure( + MediaTypeSnifferError.DataAccess(it.cause) + ) is DecoderError.DecodingError -> null } @@ -180,7 +186,7 @@ public object XhtmlMediaTypeSniffer : MediaTypeSniffer { } /** Sniffs an HTML document. */ -public object HtmlMediaTypeSniffer : MediaTypeSniffer { +public class HtmlMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("htm", "html") || @@ -192,13 +198,13 @@ public object HtmlMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { + override suspend fun sniffBlob(blob: Blob): Try { // [contentAsXml] will fail if the HTML is not a proper XML document, hence the doctype check. - resource.contentAsXml() + blob.readAsXml() .getOrElse { when (it) { - is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) + is DecoderError.DataAccess -> + return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) is DecoderError.DecodingError -> null } @@ -206,11 +212,11 @@ public object HtmlMediaTypeSniffer : MediaTypeSniffer { ?.takeIf { it.name.lowercase(Locale.ROOT) == "html" } ?.let { return Try.success(MediaType.HTML) } - resource.contentAsString() + blob.readAsString() .getOrElse { when (it) { - is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) + is DecoderError.DataAccess -> + return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) is DecoderError.DecodingError -> null @@ -260,13 +266,13 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { + override suspend fun sniffBlob(blob: Blob): Try { // OPDS 1 - resource.contentAsXml() + blob.readAsXml() .getOrElse { when (it) { - is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) + is DecoderError.DataAccess -> + return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) is DecoderError.DecodingError -> null } @@ -281,11 +287,11 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { } // OPDS 2 - resource.contentAsRwpm() + blob.readAsRwpm() .getOrElse { when (it) { - is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) + is DecoderError.DataAccess -> + return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) is DecoderError.DecodingError -> null } @@ -313,11 +319,11 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { } // OPDS Authentication Document. - resource.containsJsonKeys("id", "title", "authentication") + blob.containsJsonKeys("id", "title", "authentication") .getOrElse { when (it) { - is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) + is DecoderError.DataAccess -> + return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) is DecoderError.DecodingError -> null @@ -343,12 +349,12 @@ public object LcpLicenseMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { - resource.containsJsonKeys("id", "issued", "provider", "encryption") + override suspend fun sniffBlob(blob: Blob): Try { + blob.containsJsonKeys("id", "issued", "provider", "encryption") .getOrElse { when (it) { - is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) + is DecoderError.DataAccess -> + return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) is DecoderError.DecodingError -> null @@ -417,7 +423,7 @@ public object BitmapMediaTypeSniffer : MediaTypeSniffer { } /** Sniffs a Readium Web Manifest. */ -public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { +public class WebPubManifestMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try { if (hints.hasMediaType("application/audiobook+json")) { return Try.success(MediaType.READIUM_AUDIOBOOK_MANIFEST) @@ -434,13 +440,13 @@ public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { + public override suspend fun sniffBlob(blob: Blob): Try { val manifest: Manifest = - resource.contentAsRwpm() + blob.readAsRwpm() .getOrElse { when (it) { - is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) + is DecoderError.DataAccess -> + return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) is DecoderError.DecodingError -> null @@ -464,7 +470,7 @@ public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { } /** Sniffs a Readium Web Publication, protected or not by LCP. */ -public object WebPubMediaTypeSniffer : MediaTypeSniffer { +public class WebPubMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("audiobook") || @@ -503,33 +509,21 @@ public object WebPubMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContainer(container: ContainerMediaTypeSnifferContent): Try { + override suspend fun sniffContainer(container: Container<*>): Try { // Reads a RWPM from a manifest.json archive entry. val manifest: Manifest = - container.read(RelativeUrl("manifest.json")!!) - .getOrElse { error -> - when (error) { - is MediaTypeSnifferContentError.NotFound -> - null - else -> - return Try.failure(MediaTypeSnifferError.SourceError(error)) - } + container.get(RelativeUrl("manifest.json")!!) + ?.read() + ?.getOrElse { error -> + return Try.failure(MediaTypeSnifferError.DataAccess(error)) } ?.let { tryOrNull { Manifest.fromJSON(JSONObject(String(it))) } } ?: return Try.failure(MediaTypeSnifferError.NotRecognized) - val isLcpProtected = container.checkContains(RelativeUrl("license.lcpl")!!) - .fold( - { true }, - { error -> - when (error) { - is MediaTypeSnifferContentError.NotFound -> - false - else -> - return Try.failure(MediaTypeSnifferError.SourceError(error)) - } - } - ) + val isLcpProtected = container.contains(RelativeUrl("license.lcpl")!!) + .getOrElse { + return Try.failure(MediaTypeSnifferError.DataAccess(it)) + } if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { return if (isLcpProtected) { @@ -554,13 +548,13 @@ public object WebPubMediaTypeSniffer : MediaTypeSniffer { /** Sniffs a W3C Web Publication Manifest. */ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { - override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { + override suspend fun sniffBlob(blob: Blob): Try { // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. - val string = resource.contentAsString() + val string = blob.readAsString() .getOrElse { when (it) { - is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) + is DecoderError.DataAccess -> + return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) is DecoderError.DecodingError -> null @@ -582,7 +576,7 @@ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { * * Reference: https://www.w3.org/publishing/epub3/epub-ocf.html#sec-zip-container-mime */ -public object EpubMediaTypeSniffer : MediaTypeSniffer { +public class EpubMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("epub") || @@ -594,15 +588,12 @@ public object EpubMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContainer(container: ContainerMediaTypeSnifferContent): Try { - val mimetype = container.read(RelativeUrl("mimetype")!!) - .getOrElse { error -> - when (error) { - is MediaTypeSnifferContentError.NotFound -> - null - else -> - return Try.failure(MediaTypeSnifferError.SourceError(error)) - } + override suspend fun sniffContainer(container: Container<*>): Try { + val mimetype = container + .get(RelativeUrl("mimetype")!!) + ?.read() + ?.getOrElse { error -> + return Try.failure(MediaTypeSnifferError.DataAccess(error)) } ?.let { String(it, charset = Charsets.US_ASCII).trim() } if (mimetype == "application/epub+zip") { @@ -632,25 +623,17 @@ public object LpfMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContainer(container: ContainerMediaTypeSnifferContent): Try { - container.checkContains(RelativeUrl("index.html")!!) - .onSuccess { return Try.success(MediaType.LPF) } - .onFailure { error -> - when (error) { - is MediaTypeSnifferContentError.NotFound -> {} - else -> return Try.failure(MediaTypeSnifferError.SourceError(error)) - } - } + override suspend fun sniffContainer(container: Container<*>): Try { + container.contains(RelativeUrl("index.html")!!) + .getOrElse { return Try.failure(MediaTypeSnifferError.DataAccess(it)) } + .takeIf { it } + ?.let { return Try.success(MediaType.LPF) } // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. - container.read(RelativeUrl("publication.json")!!) - .getOrElse { error -> - when (error) { - is MediaTypeSnifferContentError.NotFound -> - null - else -> - return Try.failure(MediaTypeSnifferError.SourceError(error)) - } + container.get(RelativeUrl("publication.json")!!) + ?.read() + ?.getOrElse { error -> + return Try.failure(MediaTypeSnifferError.DataAccess(error)) } ?.let { tryOrNull { String(it) } } ?.let { manifest -> @@ -734,18 +717,22 @@ public object ArchiveMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContainer(container: ContainerMediaTypeSnifferContent): Try { + override suspend fun sniffContainer(container: Container<*>): Try { + if (container !is ClosedContainer<*>) { + return Try.failure(MediaTypeSnifferError.NotRecognized) + } + fun isIgnored(url: Url): Boolean = url.filename?.startsWith(".") == true || url.filename == "Thumbs.db" suspend fun archiveContainsOnlyExtensions(fileExtensions: List): Boolean = - container.entries()?.all { url -> + container.entries().all { url -> isIgnored(url) || url.extension?.let { fileExtensions.contains( it.lowercase(Locale.ROOT) ) } == true - } ?: false + } if (archiveContainsOnlyExtensions(cbzExtensions)) { return Try.success(MediaType.CBZ) @@ -775,10 +762,10 @@ public object PdfMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { - resource.read(0L until 5L) + override suspend fun sniffBlob(blob: Blob): Try { + blob.read(0L until 5L) .getOrElse { error -> - return Try.failure(MediaTypeSnifferError.SourceError(error)) + return Try.failure(MediaTypeSnifferError.DataAccess(error)) } .let { tryOrNull { it.toString(Charsets.UTF_8) } } .takeIf { it == "%PDF-" } @@ -798,12 +785,12 @@ public object JsonMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { - resource.contentAsJson() + override suspend fun sniffBlob(blob: Blob): Try { + blob.readAsJson() .getOrElse { when (it) { - is DecoderError.DataSourceError -> - return Try.failure(MediaTypeSnifferError.SourceError(it.cause)) + is DecoderError.DataAccess -> + return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) is DecoderError.DecodingError -> null @@ -819,7 +806,7 @@ public object JsonMediaTypeSniffer : MediaTypeSniffer { * Sniffs the system-wide registered media types using [MimeTypeMap] and * [URLConnection.guessContentTypeFromStream]. */ -public object SystemMediaTypeSniffer : MediaTypeSniffer { +public class SystemMediaTypeSniffer : MediaTypeSniffer { private val mimetypes = tryOrNull { MimeTypeMap.getSingleton() } @@ -837,16 +824,21 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { - resource.contentAsStream() - .use { + override suspend fun sniffBlob(blob: Blob): Try { + BlobInputStream(blob, ::SystemSnifferException) + .use { stream -> try { withContext(Dispatchers.IO) { - URLConnection.guessContentTypeFromStream(it) + URLConnection.guessContentTypeFromStream(stream) ?.let { sniffType(it) } } - } catch (e: MediaTypeSnifferContentException) { - return Try.failure(MediaTypeSnifferError.SourceError(e.error)) + } catch (e: Exception) { + e.findSystemSnifferException() + ?.let { + return Try.failure( + MediaTypeSnifferError.DataAccess(it.error) + ) + } } } ?.let { return Try.success(it) } @@ -854,6 +846,16 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } + private class SystemSnifferException( + val error: ReadError + ) : IOException() + private fun Throwable.findSystemSnifferException(): SystemSnifferException? = + when { + this is SystemSnifferException -> this + cause != null -> cause!!.findSystemSnifferException() + else -> null + } + private fun sniffType(type: String): MediaType? { val extension = mimetypes?.getExtensionFromMimeType(type) ?: return null diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt deleted file mode 100644 index 4e95cbc294..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSnifferContent.kt +++ /dev/null @@ -1,216 +0,0 @@ -package org.readium.r2.shared.util.mediatype - -import java.io.IOException -import java.io.InputStream -import org.json.JSONObject -import org.readium.r2.shared.extensions.read -import org.readium.r2.shared.publication.Manifest -import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.FilesystemError -import org.readium.r2.shared.util.MessageError -import org.readium.r2.shared.util.NetworkError -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.datasource.DataSource -import org.readium.r2.shared.util.datasource.DataSourceInputStream -import org.readium.r2.shared.util.datasource.DecoderError -import org.readium.r2.shared.util.datasource.readAsJson -import org.readium.r2.shared.util.datasource.readAsRwpm -import org.readium.r2.shared.util.datasource.readAsString -import org.readium.r2.shared.util.datasource.readAsXml -import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.xml.ElementNode - -/** - * Provides read access to an asset content. - */ -public sealed interface MediaTypeSnifferContent - -/** - * Provides read access to a resource content. - */ -public interface ResourceMediaTypeSnifferContent : MediaTypeSnifferContent { - - public val source: AbsoluteUrl? - - /** - * Reads all the bytes or the given [range]. - * - * It can be used to check a file signature, aka magic number. - * See https://en.wikipedia.org/wiki/List_of_file_signatures - */ - public suspend fun read(range: LongRange? = null): Try - - public suspend fun length(): Try -} - -internal fun ResourceMediaTypeSnifferContent.asDataSource() = - ResourceMediaTypeSnifferContentDataSource(this) - -internal class ResourceMediaTypeSnifferContentDataSource( - private val resourceMediaTypeSnifferContent: ResourceMediaTypeSnifferContent -) : DataSource { - - override suspend fun length(): Try = - resourceMediaTypeSnifferContent.length() - - override suspend fun read(range: LongRange?): Try = - resourceMediaTypeSnifferContent.read(range) - - override suspend fun close() { - // ResourceMediaTypeSnifferContent doesn't own the resource. - // Do nothing. - } -} - -/** - * Content as plain text. - * - * It will extract the charset parameter from the media type hints to figure out an encoding. - * Otherwise, fallback on UTF-8. - */ -internal suspend fun ResourceMediaTypeSnifferContent.contentAsString(): Try> = - asDataSource().readAsString() - -/** Content as an XML document. */ -internal suspend fun ResourceMediaTypeSnifferContent.contentAsXml(): Try> = - asDataSource().readAsXml() - -/** - * Content parsed from JSON. - */ -internal suspend fun ResourceMediaTypeSnifferContent.contentAsJson(): Try> = - asDataSource().readAsJson() - -/** Readium Web Publication Manifest parsed from the content. */ -internal suspend fun ResourceMediaTypeSnifferContent.contentAsRwpm(): Try> = - asDataSource().readAsRwpm() - -public sealed class MediaTypeSnifferContentError(override val message: String) : Error { - - public class NotFound(public override val cause: Error) : - MediaTypeSnifferContentError("Resource could not be found.") - - public class Forbidden(public override val cause: Error) : - MediaTypeSnifferContentError("You are not allowed to access this content.") - - public class Network(public override val cause: NetworkError) : - MediaTypeSnifferContentError("A network error occurred.") - - public class Filesystem(public override val cause: FilesystemError) : - MediaTypeSnifferContentError("An unexpected error occurred on filesystem.") - - public class TooBig(public override val cause: ThrowableError) : - MediaTypeSnifferContentError("Sniffing was interrupted because resource is too big.") - - public class ArchiveError(public override val cause: Error) : - MediaTypeSnifferContentError("An error occurred with archive.") - - public class Unknown(public override val cause: Error) : - MediaTypeSnifferContentError("An unknown error occurred.") -} - -/** - * Raw bytes stream of the content. - * - * A byte stream can be useful when sniffers only need to read a few bytes at the beginning of - * the file. - */ -internal fun ResourceMediaTypeSnifferContent.contentAsStream(): InputStream = - DataSourceInputStream(asDataSource(), ::MediaTypeSnifferContentException) - -internal class MediaTypeSnifferContentException( - val error: MediaTypeSnifferContentError -) : IOException() { - - companion object { - - fun Exception.unwrapMediaTypeSnifferContentException(): Exception { - this.findMediaTypeSnifferContentExceptionCause()?.let { return it } - return this - } - - private fun Throwable.findMediaTypeSnifferContentExceptionCause(): MediaTypeSnifferContentException? = - when { - this is MediaTypeSnifferContentException -> this - cause != null -> cause!!.findMediaTypeSnifferContentExceptionCause() - else -> null - } - } -} - -/** - * Returns whether the content is a JSON object containing all of the given root keys. - */ -internal suspend fun ResourceMediaTypeSnifferContent.containsJsonKeys( - vararg keys: String -): Try> { - val json = contentAsJson() - .getOrElse { return Try.failure(it) } - return Try.success(json.keys().asSequence().toSet().containsAll(keys.toList())) -} - -/** - * Provides read access to a container's resources. - */ -public interface ContainerMediaTypeSnifferContent : MediaTypeSnifferContent { - /** - * Returns all the known entry urls in the container. - */ - public suspend fun entries(): Set? - - /** - * Returns the entry data at the given [url] in this container. - */ - public suspend fun read(url: Url, range: LongRange? = null): Try - - public suspend fun length(url: Url): Try -} - -/** - * Returns whether an entry exists in the container. - */ -internal suspend fun ContainerMediaTypeSnifferContent.checkContains(url: Url): Try = - entries()?.contains(url) - ?.let { - if (it) { - Try.success(Unit) - } else { - Try.failure( - MediaTypeSnifferContentError.NotFound( - MessageError("Container entry list doesn't contain $url.") - ) - ) - } - } - ?: read(url, range = 0L..1L) - .map { } - -/** - * A [ResourceMediaTypeSnifferContent] built from a raw byte array. - */ -public class BytesResourceMediaTypeSnifferContent( - bytes: suspend () -> ByteArray -) : ResourceMediaTypeSnifferContent { - - private val bytesFactory = bytes - private lateinit var _bytes: ByteArray - - private suspend fun bytes(): ByteArray { - if (::_bytes.isInitialized) { - return _bytes - } - _bytes = bytesFactory() - return _bytes - } - - override val source: AbsoluteUrl? = null - - override suspend fun read(range: LongRange?): Try = - Try.success(bytes().read(range)) - - override suspend fun length(): Try = - Try.success(bytes().size.toLong()) -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProvider.kt deleted file mode 100644 index b515f23824..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProvider.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.resource - -import org.readium.r2.shared.util.mediatype.MediaTypeSniffer - -public interface ArchiveProvider : MediaTypeSniffer, ArchiveFactory diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt new file mode 100644 index 0000000000..e02fc8901f --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt @@ -0,0 +1,47 @@ +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.ReadError +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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.tryRecover + +internal class KnownMediaTypeResourceAdapter( + private val blob: Blob, + private val mediaType: MediaType +) : Resource, Blob by blob { + + override suspend fun mediaType(): Try = + Try.success(mediaType) + + override suspend fun properties(): Try { + return Try.success(Resource.Properties()) + } +} + +internal class GuessMediaTypeResourceAdapter( + private val blob: Blob, + private val mediaTypeRetriever: MediaTypeRetriever, + private val mediaTypeHints: MediaTypeHints +) : Resource, Blob by blob { + + override suspend fun mediaType(): Try = + mediaTypeRetriever.retrieve( + hints = mediaTypeHints, + blob = blob + ).tryRecover { error -> + when (error) { + is MediaTypeSnifferError.DataAccess -> + Try.failure(error.cause) + MediaTypeSnifferError.NotRecognized -> + Try.success(MediaType.BINARY) + } + } + + override suspend fun properties(): Try { + return Try.success(Resource.Properties()) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt index 5bdf9aed2c..edfb719528 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt @@ -10,6 +10,7 @@ import org.readium.r2.shared.extensions.coerceIn import org.readium.r2.shared.extensions.contains import org.readium.r2.shared.extensions.requireLengthFitInt import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError /** * Wraps a [Resource] and buffers its content. @@ -44,8 +45,8 @@ public class BufferingResource( */ private var buffer: Pair? = null - private lateinit var _cachedLength: ResourceTry - private suspend fun cachedLength(): ResourceTry { + private lateinit var _cachedLength: Try + private suspend fun cachedLength(): Try { if (!::_cachedLength.isInitialized) { _cachedLength = resource.length() } @@ -58,7 +59,7 @@ public class BufferingResource( } } - override suspend fun read(range: LongRange?): ResourceTry { + override suspend fun read(range: LongRange?): Try { val length = cachedLength().getOrNull() // Reading the whole resource bypasses buffering to keep things simple. if (range == null || length == null) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt index 2048b4f331..ca29dbe0c7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt @@ -13,27 +13,28 @@ import org.readium.r2.shared.extensions.requireLengthFitInt import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType public sealed class BaseBytesResource( override val source: AbsoluteUrl?, private val mediaType: MediaType, private val properties: Resource.Properties, - protected val bytes: suspend () -> Try + protected val bytes: suspend () -> Try ) : Resource { - override suspend fun properties(): ResourceTry = + override suspend fun properties(): Try = Try.success(properties) - override suspend fun mediaType(): ResourceTry = + override suspend fun mediaType(): Try = Try.success(mediaType) - override suspend fun length(): ResourceTry = + override suspend fun length(): Try = read().map { it.size.toLong() } - private lateinit var _bytes: Try + private lateinit var _bytes: Try - override suspend fun read(range: LongRange?): ResourceTry { + override suspend fun read(range: LongRange?): Try { if (!::_bytes.isInitialized) { _bytes = bytes() } @@ -62,7 +63,7 @@ public class BytesResource( source: AbsoluteUrl? = null, mediaType: MediaType, properties: Resource.Properties = Resource.Properties(), - bytes: suspend () -> ResourceTry + bytes: suspend () -> Try ) : BaseBytesResource( source = source, mediaType = mediaType, @@ -87,7 +88,7 @@ public class StringResource( source: AbsoluteUrl? = null, mediaType: MediaType, properties: Resource.Properties = Resource.Properties(), - string: suspend () -> ResourceTry + string: suspend () -> Try ) : BaseBytesResource( source = source, mediaType = mediaType, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt deleted file mode 100644 index a92f6d4ae9..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Container.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.resource - -import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.SuspendingCloseable -import org.readium.r2.shared.util.Url - -/** - * A container provides access to a list of [Resource] entries. - */ -public interface Container : SuspendingCloseable { - - /** - * Represents a container entry's. - */ - public interface Entry : Resource { - - /** - * URL used to access the resource in the container. - */ - public val url: Url - } - - /** - * Direct source to this container, when available. - */ - public val source: AbsoluteUrl? get() = null - - /** - * List of all the container entries of null if such a list is not available. - */ - public suspend fun entries(): Set? - - /** - * Returns the [Entry] at the given [url]. - * - * A [Entry] is always returned, since for some cases we can't know if it exists before actually - * fetching it, such as HTTP. Therefore, errors are handled at the Entry level. - */ - public fun get(url: Url): Entry -} - -/** A [Container] providing no resources at all. */ -public class EmptyContainer : Container { - - override suspend fun entries(): Set = emptySet() - - override fun get(url: Url): Container.Entry = - FailureResource(ResourceError.NotFound()).toEntry(url) - - override suspend fun close() {} -} - -/** A [Container] for a single [Resource]. */ -public class ResourceContainer(url: Url, resource: Resource) : Container { - - private val entry = resource.toEntry(url) - - override suspend fun entries(): Set = setOf(entry) - - override fun get(url: Url): Container.Entry { - if (url.removeFragment().removeQuery() != entry.url) { - return FailureResource(ResourceError.NotFound()).toEntry(url) - } - - return entry - } - - override suspend fun close() { - entry.close() - } -} - -/** Convenience helper to wrap a [Resource] and a [url] into a [Container.Entry]. */ -internal fun Resource.toEntry(url: Url): Container.Entry = - object : Container.Entry, Resource by this { - override val url: Url = url - } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index 51e64256cf..893b07c72c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -13,7 +13,12 @@ import org.readium.r2.shared.extensions.isParentOf import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.FileBlob +import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.toUrl /** * A file system directory as a [Container]. @@ -21,42 +26,41 @@ import org.readium.r2.shared.util.mediatype.MediaTypeRetriever public class DirectoryContainer( private val root: File, private val mediaTypeRetriever: MediaTypeRetriever -) : Container { +) : ClosedContainer { - private inner class FileEntry(override val url: Url, file: File) : - Container.Entry, Resource by FileResource(file, mediaTypeRetriever) { - - override suspend fun close() {} - } - - private val _entries: Set? by lazy { + private val _entries: Set by lazy { tryOrNull { root.walk() .filter { it.isFile } - .mapNotNull { it.toEntry() } + .mapNotNull { it.toUrl() } .toSet() - } + }.orEmpty() } - private fun File.toEntry(): Container.Entry? = - Url.fromDecodedPath(this.relativeTo(root).path) - ?.let { url -> FileEntry(url, this) } + private fun File.toEntry(): ResourceEntry? { + val url = Url.fromDecodedPath(this.relativeTo(root).path) + ?: return null - override suspend fun entries(): Set? { + val resource = GuessMediaTypeResourceAdapter( + FileBlob(this), + mediaTypeRetriever, + MediaTypeHints(fileExtension = extension) + ) + return DelegatingResourceEntry(url, resource) + } + + override suspend fun entries(): Set { return withContext(Dispatchers.IO) { _entries } } - override fun get(url: Url): Container.Entry { + override fun get(url: Url): ResourceEntry? { val file = (url as? RelativeUrl)?.path ?.let { File(root, it) } + ?.takeIf { !root.isParentOf(it) } - return if (file == null || !root.isParentOf(file)) { - FailureResource(ResourceError.NotFound()).toEntry(url) - } else { - FileEntry(url, file) - } + return file?.toEntry() } override suspend fun close() {} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt index c81c0f0568..31abd044c8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt @@ -7,6 +7,8 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType /** @@ -14,21 +16,21 @@ import org.readium.r2.shared.util.mediatype.MediaType */ public class FallbackResource( private val originalResource: Resource, - private val fallbackResourceFactory: (ResourceError) -> Resource? + private val fallbackResourceFactory: (ReadError) -> Resource? ) : Resource { override val source: AbsoluteUrl? = null - override suspend fun mediaType(): ResourceTry = + override suspend fun mediaType(): Try = withResource { mediaType() } - override suspend fun properties(): ResourceTry = + override suspend fun properties(): Try = withResource { properties() } - override suspend fun length(): ResourceTry = + override suspend fun length(): Try = withResource { length() } - override suspend fun read(range: LongRange?): ResourceTry = + override suspend fun read(range: LongRange?): Try = withResource { read(range) } override suspend fun close() { @@ -39,7 +41,7 @@ public class FallbackResource( private lateinit var _resource: Resource - private suspend fun withResource(action: suspend Resource.() -> ResourceTry): ResourceTry { + private suspend fun withResource(action: suspend Resource.() -> Try): Try { if (::_resource.isInitialized) { return _resource.action() } @@ -63,7 +65,7 @@ public class FallbackResource( * Falls back to alternative resources when the receiver fails. */ public fun Resource.fallback( - fallbackResourceFactory: (ResourceError) -> Resource? + fallbackResourceFactory: (ReadError) -> Resource? ): Resource = FallbackResource(this, fallbackResourceFactory) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt deleted file mode 100644 index acda358390..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileChannelResource.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.resource - -import java.io.FileNotFoundException -import java.nio.channels.Channels -import java.nio.channels.FileChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.readium.r2.shared.extensions.* -import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.mediatype.MediaType - -// TODO: to remove if the current approach of reading through shared storage proves good -internal class FileChannelResource( - override val source: AbsoluteUrl?, - private val channel: FileChannel -) : Resource { - - private lateinit var _length: ResourceTry - - override suspend fun mediaType(): ResourceTry = - ResourceTry.success(MediaType.BINARY) - - override suspend fun properties(): ResourceTry = - ResourceTry.success(Resource.Properties()) - - override suspend fun close() { - withContext(Dispatchers.IO) { - tryOrLog { channel.close() } - } - } - - override suspend fun read(range: LongRange?): ResourceTry = - ResourceTry.catching { - check(channel.isOpen) - if (range == null) { - return@catching readFullyThrowing() - } - - @Suppress("NAME_SHADOWING") - val range = range - .coerceFirstNonNegative() - .requireLengthFitInt() - - if (range.isEmpty()) { - return@catching ByteArray(0) - } - - readRangeThrowing(range) - } - - private suspend fun readFullyThrowing(): ByteArray = - withContext(Dispatchers.IO) { - channel.position(0) - val stream = Channels.newInputStream(channel) - stream.readFully() - } - - private suspend fun readRangeThrowing(range: LongRange): ByteArray = - withContext(Dispatchers.IO) { - channel.position(range.first) - - // The stream must not be closed here because it would close the underlying - // [FileChannel] too. Instead, [close] is responsible for that. - val stream = Channels.newInputStream(channel) - val length = range.last - range.first + 1 - stream.read(length) - } - - override suspend fun length(): ResourceTry { - if (!::_length.isInitialized) { - ResourceTry.catching { - _length = withContext(Dispatchers.IO) { - check(channel.isOpen) - Try.success(channel.size()) - } - } - } - - return _length - } - - private inline fun Try.Companion.catching(closure: () -> T): ResourceTry = - try { - success(closure()) - } catch (e: FileNotFoundException) { - failure(ResourceError.NotFound(e)) - } catch (e: SecurityException) { - failure(ResourceError.Forbidden(e)) - } catch (e: Exception) { - failure(ResourceError.Other(e)) - } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - failure(ResourceError.OutOfMemory(e)) - } - - override fun toString(): String = - "${javaClass.simpleName}(${channel.size()} bytes)" -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt index dc1dc958e1..52d66953bb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt @@ -7,19 +7,21 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType /** * Wraps a [Resource] which will be created only when first accessing one of its members. */ -public open class LazyResource( +public open class LazyResource( override val source: AbsoluteUrl? = null, - private val factory: suspend () -> R + private val factory: suspend () -> Resource ) : Resource { - private lateinit var _resource: R + private lateinit var _resource: Resource - protected suspend fun resource(): R { + protected suspend fun resource(): Resource { if (!::_resource.isInitialized) { _resource = factory() } @@ -27,16 +29,16 @@ public open class LazyResource( return _resource } - override suspend fun mediaType(): ResourceTry = + override suspend fun mediaType(): Try = resource().mediaType() - override suspend fun properties(): ResourceTry = + override suspend fun properties(): Try = resource().properties() - override suspend fun length(): ResourceTry = + override suspend fun length(): Try = resource().length() - override suspend fun read(range: LongRange?): ResourceTry = + override suspend fun read(range: LongRange?): Try = resource().read(range) override suspend fun close() { @@ -53,5 +55,5 @@ public open class LazyResource( } } -public fun Resource.flatMap(transform: suspend (Resource) -> R): LazyResource = +public fun Resource.flatMap(transform: suspend (Resource) -> R): LazyResource = LazyResource { transform(this) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeExt.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeExt.kt deleted file mode 100644 index 0ae0aea102..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeExt.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.resource - -import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.mediatype.ContainerMediaTypeSnifferContent -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferContentError -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.mediatype.ResourceMediaTypeSnifferContent -import org.readium.r2.shared.util.tryRecover - -public class ResourceMediaTypeSnifferContent( - private val resource: Resource -) : ResourceMediaTypeSnifferContent { - - override val source: AbsoluteUrl? = - resource.source - - override suspend fun read(range: LongRange?): Try = - resource.safeRead(range) - .mapFailure { it.toMediaTypeSnifferContentError() } - - override suspend fun length(): Try = - resource.length() - .mapFailure { it.toMediaTypeSnifferContentError() } -} - -public class ContainerMediaTypeSnifferContent( - private val container: Container -) : ContainerMediaTypeSnifferContent { - - override suspend fun entries(): Set? = - container.entries()?.map { it.url }?.toSet() - - override suspend fun read(url: Url, range: LongRange?): Try = - container.get(url).safeRead(range) - .mapFailure { it.toMediaTypeSnifferContentError() } - - override suspend fun length(url: Url): Try = - container.get(url).length() - .mapFailure { it.toMediaTypeSnifferContentError() } -} -private suspend fun Resource.safeRead(range: LongRange?): Try { - try { - val length = length() - .getOrElse { return Try.failure(it) } - - // We only read files smaller than 5MB to avoid an [OutOfMemoryError]. - if (range == null && length > 5 * 1000 * 1000) { - return Try.failure( - ResourceError.Other( - MessageError("Reading full content of big files is prevented.") - ) - ) - } - return read(range) - } catch (e: OutOfMemoryError) { - return Try.failure(ResourceError.OutOfMemory(e)) - } -} - -internal fun ResourceError.toMediaTypeSnifferContentError() = - when (this) { - is ResourceError.Filesystem -> - MediaTypeSnifferContentError.Filesystem(cause) - is ResourceError.Forbidden -> - MediaTypeSnifferContentError.Forbidden(this) - is ResourceError.InvalidContent -> - MediaTypeSnifferContentError.ArchiveError(this) - is ResourceError.Network -> - MediaTypeSnifferContentError.Network(cause) - is ResourceError.NotFound -> - MediaTypeSnifferContentError.NotFound(this) - is ResourceError.Other -> - MediaTypeSnifferContentError.Unknown(this) - is ResourceError.OutOfMemory -> - MediaTypeSnifferContentError.TooBig(cause) - } - -internal fun Try.toResourceTry(): ResourceTry = - tryRecover { - when (it) { - MediaTypeSnifferError.NotRecognized -> - Try.success(MediaType.BINARY) - else -> - Try.failure(it) - } - }.mapFailure { - when (it) { - MediaTypeSnifferError.NotRecognized -> - throw IllegalStateException() - is MediaTypeSnifferError.SourceError -> { - when (it.cause) { - is MediaTypeSnifferContentError.Filesystem -> - ResourceError.Filesystem(it.cause.cause) - is MediaTypeSnifferContentError.Forbidden -> - ResourceError.Forbidden(it.cause.cause) - is MediaTypeSnifferContentError.Network -> - ResourceError.Network(it.cause.cause) - is MediaTypeSnifferContentError.NotFound -> - ResourceError.NotFound(it.cause.cause) - is MediaTypeSnifferContentError.ArchiveError -> - ResourceError.InvalidContent(it) - is MediaTypeSnifferContentError.TooBig -> - ResourceError.OutOfMemory(it.cause.cause) - is MediaTypeSnifferContentError.Unknown -> - ResourceError.Other(it) - } - } - } - } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index d4842efba9..5f4d902f87 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -6,42 +6,28 @@ package org.readium.r2.shared.util.resource -import java.io.IOException import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.ErrorException -import org.readium.r2.shared.util.FilesystemError -import org.readium.r2.shared.util.MessageError -import org.readium.r2.shared.util.NetworkError -import org.readium.r2.shared.util.SuspendingCloseable -import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType -public typealias ResourceTry = Try - /** * Acts as a proxy to an actual resource by handling read access. */ -public interface Resource : SuspendingCloseable { - - /** - * URL locating this resource, if any. - */ - public val source: AbsoluteUrl? +public interface Resource : Blob { /** * Returns the resource media type if known. */ - public suspend fun mediaType(): ResourceTry + public suspend fun mediaType(): Try /** * Properties associated to the resource. * * This is opened for extensions. */ - public suspend fun properties(): ResourceTry + public suspend fun properties(): Try public class Properties( properties: Map = emptyMap() @@ -58,98 +44,18 @@ public interface Resource : SuspendingCloseable { public class Builder(properties: Map = emptyMap()) : MutableMap by properties.toMutableMap() } - - /** - * Returns data length from metadata if available, or calculated from reading the bytes otherwise. - * - * This value must be treated as a hint, as it might not reflect the actual bytes length. To get - * the real length, you need to read the whole resource. - */ - public suspend fun length(): ResourceTry - - /** - * Reads the bytes at the given range. - * - * When [range] is null, the whole content is returned. Out-of-range indexes are clamped to the - * available length automatically. - */ - public suspend fun read(range: LongRange? = null): ResourceTry -} - -/** - * Errors occurring while accessing a resource. - */ -public sealed class ResourceError( - override val message: String, - override val cause: Error? = null -) : Error { - - /** Equivalent to a 404 HTTP error. */ - public class NotFound(cause: Error? = null) : - ResourceError("Resource not found.", cause) { - - public constructor(exception: Exception) : this(ThrowableError(exception)) - } - - /** - * Equivalent to a 403 HTTP error. - * - * This can be returned when trying to read a resource protected with a DRM that is not - * unlocked. - */ - public class Forbidden(cause: Error? = null) : - ResourceError("You are not allowed to access the resource.", cause) { - public constructor(exception: Exception) : this(ThrowableError(exception)) - } - - public class Network(public override val cause: NetworkError) : - ResourceError("A network error occurred.", cause) - - public class Filesystem(public override val cause: FilesystemError) : - ResourceError("A filesystem error occurred.", cause) { - - public constructor(exception: Exception) : this(FilesystemError(exception)) - } - - /** - * Equivalent to a 507 HTTP error. - * - * Used when the requested range is too large to be read in memory. - */ - public class OutOfMemory(override val cause: ThrowableError) : - ResourceError("The resource is too large to be read on this device.", cause) { - - public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) - } - - public class InvalidContent(cause: Error? = null) : - ResourceError("Content seems invalid. ", cause) { - - public constructor(message: String) : this(MessageError(message)) - public constructor(exception: Exception) : this(ThrowableError(exception)) - } - - /** For any other error, such as HTTP 500. */ - public class Other(cause: Error) : - ResourceError("An unclassified error occurred.", cause) { - - public constructor(message: String) : this(MessageError(message)) - public constructor(exception: Exception) : this(ThrowableError(exception)) - } - - internal companion object } /** Creates a Resource that will always return the given [error]. */ public class FailureResource( - private val error: ResourceError + private val error: ReadError ) : Resource { override val source: AbsoluteUrl? = null - override suspend fun mediaType(): ResourceTry = Try.failure(error) - override suspend fun properties(): ResourceTry = Try.failure(error) - override suspend fun length(): ResourceTry = Try.failure(error) - override suspend fun read(range: LongRange?): ResourceTry = Try.failure(error) + override suspend fun mediaType(): Try = Try.failure(error) + override suspend fun properties(): Try = Try.failure(error) + override suspend fun length(): Try = Try.failure(error) + override suspend fun read(range: LongRange?): Try = Try.failure(error) override suspend fun close() {} override fun toString(): String = @@ -162,17 +68,17 @@ public class FailureResource( replaceWith = ReplaceWith("map(transform)") ) @Suppress("UnusedReceiverParameter") -public fun ResourceTry.mapCatching(): ResourceTry = +public fun Try.mapCatching(): ResourceTry = throw NotImplementedError() -public inline fun ResourceTry.flatMapCatching(transform: (value: S) -> ResourceTry): ResourceTry = +public inline fun Try.flatMapCatching( + transform: (value: S) -> ResourceTry +): ResourceTry = flatMap { try { transform(it) - } catch (e: Exception) { - Try.failure(ResourceError.Other(e)) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - Try.failure(ResourceError.OutOfMemory(e)) + Try.failure(ContainerEntry(ReadError.OutOfMemory(e))) } } @@ -182,26 +88,7 @@ internal fun Resource.withMediaType(mediaType: MediaType?): Resource { } return object : Resource by this { - override suspend fun mediaType(): ResourceTry = - ResourceTry.success(mediaType) - } -} - -internal class ResourceException( - val error: ResourceError -) : IOException(error.message, ErrorException(error)) { - - companion object { - fun Exception.unwrapResourceException(): Exception { - this.findResourceExceptionCause()?.let { return it } - return this - } - - private fun Throwable.findResourceExceptionCause(): ResourceException? = - when { - this is ResourceException -> this - cause != null -> cause!!.findResourceExceptionCause() - else -> null - } + override suspend fun mediaType(): Try = + Try.success(mediaType) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt new file mode 100644 index 0000000000..c5527cc18d --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +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.data.ClosedContainer +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ContainerEntry +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.mediatype.MediaType + +public typealias ResourceTry = Try + +public interface ResourceEntry : ContainerEntry, Resource + +public typealias ResourceContainer = Container + +public class FailureResourceEntry( + override val url: Url, + private val error: ReadError +) : ResourceEntry { + + override val source: AbsoluteUrl? = null + + override suspend fun mediaType(): ResourceTry = + Try.failure(error) + + override suspend fun properties(): ResourceTry = + Try.failure(error) + + override suspend fun length(): ResourceTry = + Try.failure(error) + + override suspend fun read(range: LongRange?): ResourceTry = + Try.failure(error) + + override suspend fun close() { + } +} + +/** A [Container] for a single [Resource]. */ +public class SingleResourceContainer( + private val url: Url, + resource: Resource +) : ClosedContainer { + public interface Entry : ResourceEntry + + private val entry = resource.toResourceEntry(url) + + override suspend fun entries(): Set = setOf(url) + + override fun get(url: Url): ResourceEntry? { + if (url.removeFragment().removeQuery() != entry.url) { + return null + } + + return entry + } + + override suspend fun close() { + entry.close() + } +} + +public class DelegatingResourceEntry( + override val url: Url, + private val resource: Resource +) : ResourceEntry, Resource by resource + +/** Convenience helper to wrap a [Resource] and a [url] into a [Container.Entry]. */ +internal fun Resource.toResourceEntry(url: Url): ResourceEntry = + DelegatingResourceEntry(url, this) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceDataSource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceDataSource.kt deleted file mode 100644 index 77bef5022e..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceDataSource.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.resource - -import android.graphics.Bitmap -import java.nio.charset.Charset -import org.json.JSONObject -import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.datasource.DataSource -import org.readium.r2.shared.util.datasource.DecoderError -import org.readium.r2.shared.util.datasource.decode -import org.readium.r2.shared.util.datasource.readAsBitmap -import org.readium.r2.shared.util.datasource.readAsJson -import org.readium.r2.shared.util.datasource.readAsString -import org.readium.r2.shared.util.datasource.readAsXml -import org.readium.r2.shared.util.xml.ElementNode - -private fun DecoderError.toResourceError() = - when (this) { - is DecoderError.DataSourceError -> - cause - is DecoderError.DecodingError -> - ResourceError.InvalidContent(cause) - } - -public suspend fun Resource.decode( - block: (value: ByteArray) -> R, - wrapException: (Exception) -> Error -): ResourceTry = - read() - .decode(block, wrapException) - .mapFailure { it.toResourceError() } - -/** - * Reads the full content as a [String]. - * - * If [charset] is null, then it falls back on UTF-8. - */ -public suspend fun Resource.readAsString(charset: Charset = Charsets.UTF_8): ResourceTry = - asDataSource().readAsString(charset).mapFailure { it.toResourceError() } - -/** - * Reads the full content as a JSON object. - */ -public suspend fun Resource.readAsJson(): ResourceTry = - asDataSource().readAsJson().mapFailure { it.toResourceError() } - -/** - * Reads the full content as an XML document. - */ -public suspend fun Resource.readAsXml(): ResourceTry = - asDataSource().readAsXml().mapFailure { it.toResourceError() } - -/** - * Reads the full content as a [Bitmap]. - */ -public suspend fun Resource.readAsBitmap(): ResourceTry = - asDataSource().readAsBitmap().mapFailure { it.toResourceError() } - -internal class ResourceDataSource( - private val resource: Resource -) : DataSource { - - override suspend fun length(): Try = - resource.length() - - override suspend fun read(range: LongRange?): Try = - resource.read() - - override suspend fun close() { - resource.close() - } -} - -internal fun Resource.asDataSource() = - ResourceDataSource(this) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceInputStream.kt deleted file mode 100644 index d272def7d0..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceInputStream.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.resource - -import java.io.FilterInputStream -import org.readium.r2.shared.util.datasource.DataSourceInputStream - -/** - * Input stream reading a [Resource]'s content. - * - * If you experience bad performances, consider wrapping the stream in a BufferedInputStream. This - * is particularly useful when streaming deflated ZIP entries. - * - * Raises [ResourceException]s when [ResourceError]s occur. - */ -public class ResourceInputStream private constructor( - dataSourceInputStream: DataSourceInputStream -) : FilterInputStream(dataSourceInputStream) { - - public constructor(resource: Resource, range: LongRange? = null) : - this(DataSourceInputStream(resource.asDataSource(), ::ResourceException, range)) -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt index df18162628..df7f8fc670 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt @@ -9,6 +9,8 @@ package org.readium.r2.shared.util.resource import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType /** @@ -24,16 +26,16 @@ public class SynchronizedResource( override val source: AbsoluteUrl? get() = resource.source - override suspend fun properties(): ResourceTry = + override suspend fun properties(): Try = mutex.withLock { resource.properties() } - override suspend fun mediaType(): ResourceTry = + override suspend fun mediaType(): Try = mutex.withLock { resource.mediaType() } - override suspend fun length(): ResourceTry = + override suspend fun length(): Try = mutex.withLock { resource.length() } - override suspend fun read(range: LongRange?): ResourceTry = + override suspend fun read(range: LongRange?): Try = mutex.withLock { resource.read(range) } override suspend fun close() { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt index 76c5e3ead8..6357322c77 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt @@ -7,6 +7,7 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ClosedContainer /** * Implements the transformation of a Resource. It can be used, for example, to decrypt, @@ -22,22 +23,25 @@ public typealias ResourceTransformer = (Resource) -> Resource * functions. */ public class TransformingContainer( - private val container: Container, + private val container: ClosedContainer, private val transformers: List -) : Container { +) : ClosedContainer { - public constructor(fetcher: Container, transformer: ResourceTransformer) : - this(fetcher, listOf(transformer)) + public constructor(container: ClosedContainer, transformer: ResourceTransformer) : + this(container, listOf(transformer)) - override suspend fun entries(): Set? = + override suspend fun entries(): Set = container.entries() - override fun get(url: Url): Container.Entry = - transformers - .fold(container.get(url) as Resource) { acc, transformer -> + override fun get(url: Url): ResourceEntry? { + val originalResource = container.get(url) + ?: return null + + return transformers + .fold(originalResource) { acc: Resource, transformer: ResourceTransformer -> transformer(acc) - } - .toEntry(url) + }.toResourceEntry(url) + } override suspend fun close() { container.close() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt index 35ffa1be67..3b9209bbcc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt @@ -9,6 +9,8 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.extensions.coerceIn import org.readium.r2.shared.extensions.requireLengthFitInt import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap /** @@ -31,21 +33,21 @@ public abstract class TransformingResource( */ public operator fun invoke( resource: Resource, - transform: suspend (ByteArray) -> ResourceTry + transform: suspend (ByteArray) -> Try ): TransformingResource = object : TransformingResource(resource) { - override suspend fun transform(data: ResourceTry): ResourceTry = + override suspend fun transform(data: Try): Try = data.flatMap { transform(it) } } } override val source: AbsoluteUrl? = null - private lateinit var _bytes: ResourceTry + private lateinit var _bytes: Try - public abstract suspend fun transform(data: ResourceTry): ResourceTry + public abstract suspend fun transform(data: Try): Try - private suspend fun bytes(): ResourceTry { + private suspend fun bytes(): Try { if (::_bytes.isInitialized) { return _bytes } @@ -58,7 +60,7 @@ public abstract class TransformingResource( return bytes } - override suspend fun read(range: LongRange?): ResourceTry = + override suspend fun read(range: LongRange?): Try = bytes().map { if (range == null) { return bytes() @@ -72,8 +74,9 @@ public abstract class TransformingResource( it.sliceArray(range.map(Long::toInt)) } - override suspend fun length(): ResourceTry = bytes().map { it.size.toLong() } + override suspend fun length(): Try = + bytes().map { it.size.toLong() } } -public fun Resource.map(transform: suspend (ByteArray) -> ResourceTry): Resource = +public fun Resource.map(transform: suspend (ByteArray) -> Try): Resource = TransformingResource(this, transform = transform) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt index c83b0a2cfe..7398d1b371 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt @@ -12,10 +12,12 @@ import org.jsoup.Jsoup import org.jsoup.parser.Parser import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.readAsString import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceTry -import org.readium.r2.shared.util.resource.readAsString +import org.readium.r2.shared.util.tryRecover /** * Extracts pure content from a marked-up (e.g. HTML) or binary (e.g. PDF) resource. @@ -26,7 +28,7 @@ public interface ResourceContentExtractor { /** * Extracts the text content of the given [resource]. */ - public suspend fun extractText(resource: Resource): ResourceTry = Try.success("") + public suspend fun extractText(resource: Resource): Try = Try.success("") public interface Factory { /** @@ -54,15 +56,22 @@ public class DefaultResourceContentExtractorFactory : ResourceContentExtractor.F @ExperimentalReadiumApi public class HtmlResourceContentExtractor : ResourceContentExtractor { - override suspend fun extractText(resource: Resource): ResourceTry = withContext( - Dispatchers.IO - ) { - resource - .readAsString() - .map { html -> - val body = Jsoup.parse(html).body().text() - // Transform HTML entities into their actual characters. - Parser.unescapeEntities(body, false) - } - } + override suspend fun extractText(resource: Resource): Try = + withContext(Dispatchers.IO) { + resource + .readAsString() + .tryRecover { + when (it) { + is DecoderError.DataAccess -> + return@withContext Try.failure(it.cause) + is DecoderError.DecodingError -> + Try.success("") + } + } + .map { html -> + val body = Jsoup.parse(html).body().text() + // Transform HTML entities into their actual characters. + Parser.unescapeEntities(body, false) + } + } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index 717cc96f31..bf7ea7e6b6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -14,48 +14,44 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.archive.ArchiveProperties +import org.readium.r2.shared.util.archive.archive +import org.readium.r2.shared.util.data.AccessException +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.unwrapAccessException import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream 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 org.readium.r2.shared.util.resource.ArchiveProperties -import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.FailureResource +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceError -import org.readium.r2.shared.util.resource.ResourceException -import org.readium.r2.shared.util.resource.ResourceException.Companion.unwrapResourceException -import org.readium.r2.shared.util.resource.ResourceMediaTypeSnifferContent +import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.resource.ResourceTry -import org.readium.r2.shared.util.resource.archive -import org.readium.r2.shared.util.resource.toResourceTry +import org.readium.r2.shared.util.tryRecover import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipArchiveEntry import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile internal class ChannelZipContainer( - private val archive: ZipFile, + private val zipFile: ZipFile, override val source: AbsoluteUrl?, private val mediaTypeRetriever: MediaTypeRetriever -) : Container { - - private inner class FailureEntry( - override val url: Url - ) : Container.Entry, Resource by FailureResource(ResourceError.NotFound()) +) : ClosedContainer { private inner class Entry( override val url: Url, private val entry: ZipArchiveEntry - ) : Container.Entry { + ) : ResourceEntry { override val source: AbsoluteUrl? get() = null override suspend fun properties(): ResourceTry = - ResourceTry.success( + Try.success( Resource.Properties { archive = ArchiveProperties( entryLength = compressedLength - ?: length().getOrElse { return ResourceTry.failure(it) }, + ?: length().getOrElse { return Try.failure(it) }, isEntryCompressed = compressedLength != null ) } @@ -64,13 +60,20 @@ internal class ChannelZipContainer( override suspend fun mediaType(): ResourceTry = mediaTypeRetriever.retrieve( hints = MediaTypeHints(fileExtension = url.extension), - content = ResourceMediaTypeSnifferContent(this) - ).toResourceTry() + blob = this + ).tryRecover { error -> + when (error) { + is MediaTypeSnifferError.DataAccess -> + Try.failure(error.cause) + MediaTypeSnifferError.NotRecognized -> + Try.success(MediaType.BINARY) + } + } override suspend fun length(): ResourceTry = entry.size.takeUnless { it == -1L } ?.let { Try.success(it) } - ?: Try.failure(ResourceError.Other(UnsupportedOperationException())) + ?: Try.failure(ReadError.Other(UnsupportedOperationException())) private val compressedLength: Long? get() = @@ -91,17 +94,17 @@ internal class ChannelZipContainer( } Try.success(bytes) } catch (exception: Exception) { - when (val e = exception.unwrapResourceException()) { - is ResourceException -> + when (val e = exception.unwrapAccessException()) { + is AccessException -> Try.failure(e.error) else -> - Try.failure(ResourceError.InvalidContent(e)) + Try.failure(ReadError.Content(e)) } } } private suspend fun readFully(): ByteArray = - archive.getInputStream(entry).use { + zipFile.getInputStream(entry).use { it.readFully() } @@ -124,7 +127,7 @@ internal class ChannelZipContainer( */ private fun stream(fromIndex: Long): CountingInputStream { if (entry.method == ZipArchiveEntry.STORED && fromIndex < entry.size) { - return CountingInputStream(archive.getRawInputStream(entry, fromIndex), fromIndex) + return CountingInputStream(zipFile.getRawInputStream(entry, fromIndex), fromIndex) } // Reuse the current stream if it didn't exceed the requested index. @@ -134,7 +137,7 @@ internal class ChannelZipContainer( stream?.close() - return CountingInputStream(archive.getInputStream(entry)) + return CountingInputStream(zipFile.getInputStream(entry)) .also { stream = it } } @@ -149,25 +152,21 @@ internal class ChannelZipContainer( } } - override suspend fun entries(): Set = - archive.entries.toList() + override suspend fun entries(): Set = + zipFile.entries.toList() .filterNot { it.isDirectory } - .mapNotNull { entry -> - Url.fromDecodedPath(entry.name) - ?.let { url -> Entry(url, entry) } - } + .mapNotNull { entry -> Url.fromDecodedPath(entry.name) } .toSet() - override fun get(url: Url): Container.Entry = + override fun get(url: Url): ResourceEntry? = (url as? RelativeUrl)?.path - ?.let { archive.getEntry(it) } + ?.let { zipFile.getEntry(it) } ?.takeUnless { it.isDirectory } ?.let { Entry(url, it) } - ?: FailureEntry(url) override suspend fun close() { withContext(Dispatchers.IO) { - tryOrLog { archive.close() } + tryOrLog { zipFile.close() } } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/DatasourceChannel.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/DatasourceChannel.kt index 4162fddbd8..6701311320 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/DatasourceChannel.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/DatasourceChannel.kt @@ -15,14 +15,14 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.datasource.DataSource +import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.zip.jvm.ClosedChannelException import org.readium.r2.shared.util.zip.jvm.NonWritableChannelException import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel internal class DatasourceChannel( - private val dataSource: DataSource, + private val blob: Blob, private val wrapError: (E) -> IOException ) : SeekableByteChannel { @@ -41,7 +41,7 @@ internal class DatasourceChannel( } isClosed = true - coroutineScope.launch { dataSource.close() } + coroutineScope.launch { blob.close() } } override fun isOpen(): Boolean { @@ -55,7 +55,7 @@ internal class DatasourceChannel( } withContext(Dispatchers.IO) { - val size = dataSource.length() + val size = blob.length() .mapFailure(wrapError) .getOrThrow() @@ -66,7 +66,7 @@ internal class DatasourceChannel( val available = size - position val toBeRead = dst.remaining().coerceAtMost(available.toInt()) check(toBeRead > 0) - val bytes = dataSource.read(position until position + toBeRead) + val bytes = blob.read(position until position + toBeRead) .mapFailure(wrapError) .getOrThrow() check(bytes.size == toBeRead) @@ -99,7 +99,7 @@ internal class DatasourceChannel( throw ClosedChannelException() } - return runBlocking { dataSource.length() } + return runBlocking { blob.length() } .mapFailure { wrapError(it) } .getOrThrow() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/HttpChannel.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/HttpChannel.kt deleted file mode 100644 index 7768fbcf8e..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/HttpChannel.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.zip - -import java.io.IOException -import java.io.InputStream -import java.nio.ByteBuffer -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.extensions.readSafe -import org.readium.r2.shared.extensions.tryOrLog -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.assertSuccess -import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpError -import org.readium.r2.shared.util.http.HttpRequest -import org.readium.r2.shared.util.http.HttpResponse -import org.readium.r2.shared.util.http.head -import org.readium.r2.shared.util.io.CountingInputStream -import org.readium.r2.shared.util.zip.jvm.NonWritableChannelException -import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel -import timber.log.Timber - -@OptIn(ExperimentalReadiumApi::class) -internal class HttpChannel( - private val url: String, - private val client: HttpClient, - private val maxSkipBytes: Long = 8192 -) : SeekableByteChannel { - - private var position: Long = 0 - - private val lock = Any() - - private var inputStream: CountingInputStream? = null - - private var inputStreamStart = 0L - - /** Cached HEAD response to get the expected content length and other metadata. */ - private lateinit var _headResponse: Try - - private suspend fun headResponse(): Try { - if (::_headResponse.isInitialized) { - return _headResponse - } - - _headResponse = client.head(HttpRequest(url)) - return _headResponse - } - - /** - * Returns an HTTP stream for the resource, starting at the [from] byte offset. - * - * The stream is cached and reused for next calls, if the next [from] offset is in a forward - * direction. - */ - private suspend fun stream(from: Long? = null): Try { - Timber.d("getStream") - val stream = inputStream - if (from != null && stream != null) { - tryOrLog { - val bytesToSkip = from - (inputStreamStart + stream.count) - if (bytesToSkip in 0 until maxSkipBytes) { - stream.skip(bytesToSkip) - Timber.d("reusing stream") - return Try.success(stream) - } - } - } - - tryOrLog { inputStream?.close() } - - val request = HttpRequest(url) { - from?.takeUnless { it == 0L } - ?.let { setRange(from..-1) } - } - - Timber.d("request ${request.headers}") - - return client.stream(request) - .map { - Timber.d("responseCode ${it.response.statusCode}") - Timber.d("response ${it.response}") - Timber.d("responseHeaders ${it.response.headers}") - CountingInputStream(it.body) - } - .onSuccess { - inputStream = it - inputStreamStart = from ?: 0 - } - } - - override fun close() {} - - override fun isOpen(): Boolean { - return true - } - - override fun read(dst: ByteBuffer): Int { - synchronized(lock) { - return runBlocking { - withContext(Dispatchers.IO) { - val size = headResponse() - .map { it.contentLength } - .assertSuccess() - ?: throw IOException("Server didn't provide content length.") - - if (position >= size) { - return@withContext -1 - } - - Timber.d("position $position") - val available = size - position - val buffer = ByteArray(dst.remaining().coerceAtMost(available.toInt())) - Timber.d("bufferSize ${buffer.size}") - val read = stream(position) - .assertSuccess() - .readSafe(buffer) - Timber.d("read $read") - if (read != -1) { - dst.put(buffer, 0, read) - position += read - } - return@withContext read - } - } - } - } - - override fun write(src: ByteBuffer): Int { - throw NonWritableChannelException() - } - - override fun position(): Long { - synchronized(lock) { - return position - } - } - - override fun position(newPosition: Long): HttpChannel { - synchronized(lock) { - if (newPosition < 0) { - throw IllegalArgumentException("Requested position is negative.") - } - - position = newPosition - return this - } - } - - override fun size(): Long { - return synchronized(lock) { - runBlocking { headResponse() } - .assertSuccess() - .contentLength - ?: throw IOException("Unknown file length.") - } - } - - override fun truncate(size: Long): HttpChannel { - throw NonWritableChannelException() - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 38ff67b3d2..9c8c69c5ac 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -11,25 +11,20 @@ import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.datasource.DataSource +import org.readium.r2.shared.util.archive.ArchiveFactory +import org.readium.r2.shared.util.archive.ArchiveProvider +import org.readium.r2.shared.util.data.AccessException +import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.unwrapAccessException 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferContentException -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferContentException.Companion.unwrapMediaTypeSnifferContentException import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.mediatype.ResourceMediaTypeSnifferContent -import org.readium.r2.shared.util.mediatype.asDataSource -import org.readium.r2.shared.util.resource.ArchiveFactory -import org.readium.r2.shared.util.resource.ArchiveProvider -import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceError -import org.readium.r2.shared.util.resource.ResourceException -import org.readium.r2.shared.util.resource.ResourceException.Companion.unwrapResourceException -import org.readium.r2.shared.util.resource.asDataSource +import org.readium.r2.shared.util.resource.ResourceContainer +import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel @@ -52,16 +47,14 @@ public class StreamingZipArchiveProvider( return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffResource(resource: ResourceMediaTypeSnifferContent): Try { - val datasource = resource.asDataSource() - + override suspend fun sniffBlob(blob: Blob): Try { return try { - openDataSource(datasource, ::MediaTypeSnifferContentException, null) + openDataSource(blob, ::AccessException, null) Try.success(MediaType.ZIP) } catch (exception: Exception) { - when (val e = exception.unwrapMediaTypeSnifferContentException()) { - is MediaTypeSnifferContentException -> - Try.failure(MediaTypeSnifferError.SourceError(e.error)) + when (val e = exception.unwrapAccessException()) { + is AccessException -> + Try.failure(MediaTypeSnifferError.DataAccess(e.error)) else -> Try.failure(MediaTypeSnifferError.NotRecognized) } @@ -69,42 +62,42 @@ public class StreamingZipArchiveProvider( } override suspend fun create( - resource: Resource, + resource: Blob, password: String? - ): Try { + ): Try, ArchiveFactory.Error> { if (password != null) { return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) } return try { val container = openDataSource( - resource.asDataSource(), - ::ResourceException, + resource, + ::AccessException, resource.source ) Try.success(container) } catch (exception: Exception) { - when (val e = exception.unwrapResourceException()) { - is ResourceException -> + when (val e = exception.unwrapAccessException()) { + is AccessException -> Try.failure(ArchiveFactory.Error.ResourceError(e.error)) else -> - Try.failure(ArchiveFactory.Error.ResourceError(ResourceError.InvalidContent(e))) + Try.failure(ArchiveFactory.Error.ResourceError(ReadError.Content(e))) } } } - private suspend fun openDataSource( - dataSource: DataSource, - wrapError: (E) -> IOException, + private suspend fun openDataSource( + blob: Blob, + wrapError: (ReadError) -> IOException, sourceUrl: AbsoluteUrl? - ): Container = withContext(Dispatchers.IO) { - val datasourceChannel = DatasourceChannel(dataSource, wrapError) + ): ClosedContainer = withContext(Dispatchers.IO) { + val datasourceChannel = DatasourceChannel(blob, wrapError) val channel = wrapBaseChannel(datasourceChannel) val zipFile = ZipFile(channel, true) ChannelZipContainer(zipFile, sourceUrl, mediaTypeRetriever) } - internal suspend fun openFile(file: File): Container = withContext(Dispatchers.IO) { + internal suspend fun openFile(file: File): ResourceContainer = withContext(Dispatchers.IO) { val fileChannel = FileChannelAdapter(file, "r") val channel = wrapBaseChannel(fileChannel) ChannelZipContainer(ZipFile(channel), file.toUrl(), mediaTypeRetriever) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt index 62425e30d4..c0c3d4505b 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt @@ -12,7 +12,6 @@ import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.robolectric.RobolectricTestRunner diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt index 418cfc2568..4cb3c70976 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt @@ -37,7 +37,7 @@ class TestContainer(resources: Map = emptyMap()) : Container { override suspend fun mediaType(): ResourceTry = Try.failure(Resource.Error.NotFound()) - override suspend fun properties(): ResourceTry = + override suspend fun properties(): ResourceTry = Try.failure(Resource.Error.NotFound()) override suspend fun length(): ResourceTry = diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt index 3f7fafd821..9d79c07479 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt @@ -22,8 +22,8 @@ import org.readium.r2.shared.publication.* import org.readium.r2.shared.readBlocking import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.FileResource import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toAbsoluteUrl import org.robolectric.RobolectricTestRunner @@ -61,7 +61,7 @@ class CoverServiceTest { ), container = ResourceContainer( coverPath, - FileResource(coverPath.toFile()!!, mediaType = MediaType.JPEG) + FileBlob(coverPath.toFile()!!, mediaType = MediaType.JPEG) ) ) } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt index 4b3113aacf..1cf3efe29d 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt @@ -10,7 +10,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.FileZipArchiveProvider +import org.readium.r2.shared.util.archive.FileZipArchiveProvider import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt index 1bf64d0fdc..e9f6a805a7 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures +import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @@ -122,7 +123,7 @@ class BufferingResourceTest { private val file = Fixtures("util/resource").fileAt("epub.epub") private val data = file.readBytes() - private val resource = FileResource(file, MediaType.EPUB) + private val resource = FileBlob(file, MediaType.EPUB) private fun sut(bufferSize: Long = 1024): BufferingResource = BufferingResource(resource, bufferSize = bufferSize) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt index 4d2c7c2205..8f6be1d7d1 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt @@ -21,6 +21,7 @@ import org.junit.runner.RunWith import org.readium.r2.shared.lengthBlocking import org.readium.r2.shared.readBlocking import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.toAbsoluteUrl import org.robolectric.RobolectricTestRunner @@ -42,7 +43,7 @@ class DirectoryContainerTest { @Test fun `Reading a missing file returns NotFound`() { val resource = sut().get(Url("unknown")!!) - assertIs(resource.readBlocking().failureOrNull()) + assertIs(resource.readBlocking().failureOrNull()) } @Test @@ -62,13 +63,13 @@ class DirectoryContainerTest { @Test fun `Reading a directory returns NotFound`() { val resource = sut().get(Url("subdirectory")!!) - assertIs(resource.readBlocking().failureOrNull()) + assertIs(resource.readBlocking().failureOrNull()) } @Test fun `Reading a file outside the allowed directory returns NotFound`() { val resource = sut().get(Url("../epub.epub")!!) - assertIs(resource.readBlocking().failureOrNull()) + assertIs(resource.readBlocking().failureOrNull()) } @Test @@ -114,13 +115,13 @@ class DirectoryContainerTest { @Test fun `Computing a directory length returns NotFound`() { val resource = sut().get(Url("subdirectory")!!) - assertIs(resource.lengthBlocking().failureOrNull()) + assertIs(resource.lengthBlocking().failureOrNull()) } @Test fun `Computing the length of a missing file returns NotFound`() { val resource = sut().get(Url("unknown")!!) - assertIs(resource.lengthBlocking().failureOrNull()) + assertIs(resource.lengthBlocking().failureOrNull()) } @Test diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourcePropertiesTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt similarity index 93% rename from readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourcePropertiesTest.kt rename to readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt index 14f495a605..168e007be3 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourcePropertiesTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt @@ -6,11 +6,12 @@ import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.assertJSONEquals -import org.readium.r2.shared.util.resource.Resource.Properties +import org.readium.r2.shared.util.archive.ArchiveProperties +import org.readium.r2.shared.util.archive.archive import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class ResourcePropertiesTest { +class PropertiesTest { @Test fun `get no archive`() { diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt index 2e5351bd28..cf839544de 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt @@ -6,6 +6,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue import org.junit.Test import org.junit.runner.RunWith +import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @@ -20,7 +21,7 @@ class ResourceInputStreamTest { @Test fun `stream can be read by chunks`() { - val resource = FileResource(file, mediaType = MediaType.EPUB) + val resource = FileBlob(file, mediaType = MediaType.EPUB) val resourceStream = ResourceInputStream(resource) val outputStream = ByteArrayOutputStream(fileContent.size) resourceStream.copyTo(outputStream, bufferSize = bufferSize) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt index b79b5d3945..b2db991e05 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -19,7 +19,9 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.archive.FileZipArchiveProvider import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.use @@ -41,7 +43,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { assertNotNull( FileZipArchiveProvider(MediaTypeRetriever()) .create( - FileResource(File(epubZip.path), mediaType = MediaType.EPUB), + FileBlob(File(epubZip.path), mediaType = MediaType.EPUB), password = null ) .getOrNull() diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index 0540ef237b..960bbde660 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -7,38 +7,46 @@ package org.readium.r2.streamer import org.readium.r2.shared.extensions.addPrefix -import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.asset.AssetError -import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.RoutingClosedContainer +import org.readium.r2.shared.util.data.readAsRwpm import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpContainer import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceContainer -import org.readium.r2.shared.util.resource.ResourceError -import org.readium.r2.shared.util.resource.ResourceTry -import org.readium.r2.shared.util.resource.RoutingContainer -import org.readium.r2.shared.util.resource.readAsJson +import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.streamer.parser.PublicationParser import timber.log.Timber internal class ParserAssetFactory( private val httpClient: HttpClient, - private val mediaTypeRetriever: MediaTypeRetriever, private val formatRegistry: FormatRegistry ) { + sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { + + class ReadError( + override val cause: org.readium.r2.shared.util.data.ReadError + ) : Error("An error occurred while trying to read asset.", cause) + + class UnsupportedAsset( + override val cause: org.readium.r2.shared.util.Error? + ) : Error("Asset is not supported.", cause) + } + suspend fun createParserAsset( asset: Asset - ): Try { + ): Try { return when (asset) { is Asset.Container -> createParserAssetForContainer(asset) @@ -49,7 +57,7 @@ internal class ParserAssetFactory( private fun createParserAssetForContainer( asset: Asset.Container - ): Try = + ): Try = Try.success( PublicationParser.Asset( mediaType = asset.mediaType, @@ -59,7 +67,7 @@ internal class ParserAssetFactory( private suspend fun createParserAssetForResource( asset: Asset.Resource - ): Try = + ): Try = if (asset.mediaType.isRwpm) { createParserAssetForManifest(asset) } else { @@ -68,10 +76,15 @@ internal class ParserAssetFactory( private suspend fun createParserAssetForManifest( asset: Asset.Resource - ): Try { + ): Try { val manifest = asset.resource.readAsRwpm() - .mapFailure { AssetError.InvalidAsset(it) } - .getOrElse { return Try.failure(it) } + .mapFailure { + when (it) { + is DecoderError.DecodingError -> ReadError.Content(it.cause) + is DecoderError.DataAccess -> it.cause + } + } + .getOrElse { return Try.failure(Error.ReadError(it)) } val baseUrl = manifest.linkWithRel("self")?.href?.resolve() if (baseUrl == null) { @@ -79,25 +92,31 @@ internal class ParserAssetFactory( } else { if (baseUrl !is AbsoluteUrl) { return Try.failure( - AssetError.InvalidAsset("Self link is not absolute.") + Error.ReadError( + ReadError.Content("Self link is not absolute.") + ) ) } if (!baseUrl.isHttp) { return Try.failure( - AssetError.UnsupportedAsset( - "Self link doesn't use the HTTP(S) scheme." + Error.UnsupportedAsset( + MessageError("Self link doesn't use the HTTP(S) scheme.") ) ) } } + val resources = (manifest.readingOrder + manifest.resources) + .map { it.url() } + .toSet() + val container = - RoutingContainer( - local = ResourceContainer( + RoutingClosedContainer( + local = SingleResourceContainer( url = Url("manifest.json")!!, asset.resource ), - remote = HttpContainer(httpClient, baseUrl) + remote = HttpContainer(httpClient, baseUrl, resources) ) return Try.success( @@ -110,13 +129,13 @@ internal class ParserAssetFactory( private fun createParserAssetForContent( asset: Asset.Resource - ): Try { + ): Try { // Historically, the reading order of a standalone file contained a single link with the // HREF "/$assetName". This was fragile if the asset named changed, or was different on // other devices. To avoid this, we now use a single link with the HREF // "publication.extension". val extension = formatRegistry.fileExtension(asset.mediaType)?.addPrefix(".") ?: "" - val container = ResourceContainer( + val container = SingleResourceContainer( Url("publication$extension")!!, asset.resource ) @@ -128,19 +147,4 @@ internal class ParserAssetFactory( ) ) } - - private suspend fun Resource.readAsRwpm(): ResourceTry = - readAsJson() - .flatMap { json -> - Manifest.fromJSON( - json, - mediaTypeRetriever = mediaTypeRetriever - )?.let { manifest -> - Try.success(manifest) - } ?: Try.failure( - ResourceError.InvalidContent( - MessageError("Failed to parse the RWPM Manifest.") - ) - ) - } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index b7b97e8a68..d7fae789f1 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -12,9 +12,9 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.protection.AdeptFallbackContentProtection import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.publication.protection.LcpFallbackContentProtection +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpClient @@ -29,8 +29,6 @@ import org.readium.r2.streamer.parser.image.ImageParser import org.readium.r2.streamer.parser.pdf.PdfParser import org.readium.r2.streamer.parser.readium.ReadiumWebPubParser -internal typealias AssetTry = Try - /** * Opens a Publication using a list of parsers. * @@ -60,6 +58,23 @@ public class PublicationFactory( pdfFactory: PdfDocumentFactory<*>?, private val onCreatePublication: Publication.Builder.() -> Unit = {} ) { + public sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { + + public class ReadError( + override val cause: org.readium.r2.shared.util.data.ReadError + ) : Error("An error occurred while trying to read asset.", cause) + + public class UnsupportedAsset( + override val cause: org.readium.r2.shared.util.Error? + ) : Error("Asset is not supported.", cause) + + public class UnsupportedContentProtection( + override val cause: org.readium.r2.shared.util.Error? = null + ) : Error("No ContentProtection available to open asset.", cause) + } public companion object { public operator fun invoke( @@ -67,13 +82,12 @@ public class PublicationFactory( contentProtections: List = emptyList(), onCreatePublication: Publication.Builder.() -> Unit ): PublicationFactory { - val mediaTypeRetriever = MediaTypeRetriever() return PublicationFactory( context = context, contentProtections = contentProtections, formatRegistry = FormatRegistry(), - mediaTypeRetriever = mediaTypeRetriever, - httpClient = DefaultHttpClient(mediaTypeRetriever), + mediaTypeRetriever = MediaTypeRetriever(), + httpClient = DefaultHttpClient(MediaTypeRetriever()), pdfFactory = null, onCreatePublication = onCreatePublication ) @@ -82,7 +96,7 @@ public class PublicationFactory( private val contentProtections: Map = buildList { - add(LcpFallbackContentProtection(mediaTypeRetriever)) + add(LcpFallbackContentProtection()) add(AdeptFallbackContentProtection()) addAll(contentProtections.asReversed()) }.associateBy(ContentProtection::scheme) @@ -91,7 +105,7 @@ public class PublicationFactory( listOfNotNull( EpubParser(mediaTypeRetriever), pdfFactory?.let { PdfParser(context, it) }, - ReadiumWebPubParser(context, pdfFactory, mediaTypeRetriever), + ReadiumWebPubParser(context, pdfFactory), ImageParser(), AudioParser() ) @@ -100,7 +114,7 @@ public class PublicationFactory( if (!ignoreDefaultParsers) defaultParsers else emptyList() private val parserAssetFactory: ParserAssetFactory = - ParserAssetFactory(httpClient, mediaTypeRetriever, formatRegistry) + ParserAssetFactory(httpClient, formatRegistry) /** * Opens a [Publication] from the given asset. @@ -134,7 +148,7 @@ public class PublicationFactory( allowUserInteraction: Boolean, onCreatePublication: Publication.Builder.() -> Unit = {}, warnings: WarningLogger? = null - ): AssetTry { + ): Try { val compositeOnCreatePublication: Publication.Builder.() -> Unit = { this@PublicationFactory.onCreatePublication(this) onCreatePublication(this) @@ -162,8 +176,16 @@ public class PublicationFactory( asset: Asset, onCreatePublication: Publication.Builder.() -> Unit, warnings: WarningLogger? - ): Try { + ): Try { val parserAsset = parserAssetFactory.createParserAsset(asset) + .mapFailure { + when (it) { + is ParserAssetFactory.Error.ReadError -> + Error.ReadError(it.cause) + is ParserAssetFactory.Error.UnsupportedAsset -> + Error.UnsupportedAsset(it.cause) + } + } .getOrElse { return Try.failure(it) } return openParserAsset(parserAsset, onCreatePublication, warnings) } @@ -175,11 +197,19 @@ public class PublicationFactory( allowUserInteraction: Boolean, onCreatePublication: Publication.Builder.() -> Unit, warnings: WarningLogger? - ): Try { + ): Try { val protectedAsset = contentProtections[contentProtectionScheme] ?.open(asset, credentials, allowUserInteraction) + ?.mapFailure { + when (it) { + is ContentProtection.Error.AccessError -> + Error.ReadError(it.cause) + is ContentProtection.Error.UnsupportedAsset -> + Error.UnsupportedAsset(it) + } + } ?.getOrElse { return Try.failure(it) } - ?: return Try.failure(AssetError.Forbidden()) + ?: return Try.failure(Error.UnsupportedContentProtection()) val parserAsset = PublicationParser.Asset( protectedAsset.mediaType, @@ -198,7 +228,7 @@ public class PublicationFactory( publicationAsset: PublicationParser.Asset, onCreatePublication: Publication.Builder.() -> Unit = {}, warnings: WarningLogger? = null - ): Try { + ): Try { val builder = parse(publicationAsset, warnings) .getOrElse { return Try.failure(wrapParserException(it)) } @@ -224,21 +254,11 @@ public class PublicationFactory( return Try.failure(PublicationParser.Error.UnsupportedFormat()) } - private fun wrapParserException(e: PublicationParser.Error): AssetError = + private fun wrapParserException(e: PublicationParser.Error): Error = when (e) { is PublicationParser.Error.UnsupportedFormat -> - AssetError.UnsupportedAsset("Cannot find a parser for this asset.") - is PublicationParser.Error.InvalidAsset -> - AssetError.InvalidAsset(e) - is PublicationParser.Error.Filesystem -> - AssetError.Filesystem(e.cause) - is PublicationParser.Error.Forbidden -> - AssetError.Forbidden(e.cause) - is PublicationParser.Error.Network -> - AssetError.Network(e.cause) - is PublicationParser.Error.Other -> - AssetError.Unknown(e) - is PublicationParser.Error.OutOfMemory -> - AssetError.OutOfMemory(e.cause) + Error.UnsupportedAsset(MessageError("Cannot find a parser for this asset.")) + is PublicationParser.Error.ReadError -> + Error.ReadError(e.cause) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt index 6b2de45f03..02fe5a44c5 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt @@ -6,16 +6,13 @@ package org.readium.r2.streamer.parser +import kotlin.String import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.FilesystemError -import org.readium.r2.shared.util.MessageError -import org.readium.r2.shared.util.NetworkError -import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.publication.PublicationContainer +import org.readium.r2.shared.util.Error as BaseError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.ResourceError /** * Parses a Publication from an asset. @@ -32,7 +29,7 @@ public interface PublicationParser { */ public data class Asset( val mediaType: MediaType, - val container: Container + val container: PublicationContainer ) /** @@ -48,63 +45,15 @@ public interface PublicationParser { warnings: WarningLogger? = null ): Try - public sealed class Error(public override val message: String) : - org.readium.r2.shared.util.Error { + public sealed class Error( + public override val message: String, + public override val cause: org.readium.r2.shared.util.Error? + ) : BaseError { public class UnsupportedFormat : - Error("Asset format not supported.") { + Error("Asset format not supported.", null) - override val cause: org.readium.r2.shared.util.Error? = - null - } - - public class InvalidAsset(override val cause: org.readium.r2.shared.util.Error?) : - Error("An error occurred while parsing the publication.") { - - public constructor(message: String) : this(MessageError(message)) - } - - public class Forbidden(public override val cause: org.readium.r2.shared.util.Error?) : - Error("Access to some content was forbidden.") - - public class Network(public override val cause: NetworkError) : - Error("A network error occurred.") - - public class Filesystem(public override val cause: FilesystemError) : - Error("A filesystem error occurred.") - - public class OutOfMemory(override val cause: ThrowableError) : - Error("The resource is too large to be read on this device.") { - - public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) - } - - /** For any other error, such as HTTP 500. */ - public class Other(public override val cause: org.readium.r2.shared.util.Error) : - Error("A service error occurred") { - - public constructor(exception: Exception) : this(ThrowableError(exception)) - } - - internal companion object { - - fun ResourceError.toParserError() = - when (this) { - is ResourceError.Filesystem -> - Filesystem(cause) - is ResourceError.Forbidden -> - Forbidden(cause) - is ResourceError.InvalidContent -> - InvalidAsset(this) - is ResourceError.Network -> - Network(cause) - is ResourceError.NotFound -> - InvalidAsset(this) - is ResourceError.Other -> - Other(this) - is ResourceError.OutOfMemory -> - OutOfMemory(cause) - } - } + public class ReadError(override val cause: org.readium.r2.shared.util.data.ReadError) : + Error("An error occurred while trying to read asset.", cause) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index 99fc9ddb39..cd92d042aa 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -10,8 +10,10 @@ import org.readium.r2.shared.publication.LocalizedString import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Metadata import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.streamer.extensions.guessTitle @@ -37,19 +39,23 @@ public class AudioParser : PublicationParser { val readingOrder = if (asset.mediaType.matches(MediaType.ZAB)) { - (asset.container.entries() ?: emptySet()) - .filter { entry -> zabCanContain(entry.url) } - .sortedBy { it.url.toString() } + asset.container.entries() + .filter { zabCanContain(it) } + .sortedBy { it.toString() } .toMutableList() } else { listOfNotNull( - asset.container.entries()?.firstOrNull() + asset.container.entries().firstOrNull() ) } if (readingOrder.isEmpty()) { return Try.failure( - PublicationParser.Error.InvalidAsset("No audio file found in the publication.") + PublicationParser.Error.ReadError( + ReadError.Content( + MessageError("No audio file found in the publication.") + ) + ) ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ClockValueParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ClockValueParser.kt index 3484ab7b0a..e5d0516a06 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ClockValueParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ClockValueParser.kt @@ -29,8 +29,8 @@ internal object ClockValueParser { private fun parseClockvalue(value: String): Double? { val parts = value.split(":").map { it.toDoubleOrNull() ?: return null } - val min_sec = parts.last() + parts[parts.size - 2] * 60 - return if (parts.size > 2) min_sec + parts[parts.size - 3] * 3600 else min_sec + val minSec = parts.last() + parts[parts.size - 2] * 60 + return if (parts.size > 2) minSec + parts[parts.size - 3] * 3600 else minSec } private fun parseTimecount(value: Double, metric: String): Double? = diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 0265d63c55..4cc9d6af0e 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -14,19 +14,23 @@ import org.readium.r2.shared.publication.encryption.encryption import org.readium.r2.shared.publication.services.content.DefaultContentService import org.readium.r2.shared.publication.services.content.iterators.HtmlResourceContentIterator import org.readium.r2.shared.publication.services.search.StringSearchService +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.readAsXml import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever -import org.readium.r2.shared.util.resource.Container +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.resource.TransformingContainer -import org.readium.r2.shared.util.resource.readAsXml import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.readAsXmlOrNull import org.readium.r2.streamer.parser.PublicationParser -import org.readium.r2.streamer.parser.PublicationParser.Error.Companion.toParserError import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref /** @@ -52,10 +56,24 @@ public class EpubParser( val opfPath = getRootFilePath(asset.container) .getOrElse { return Try.failure(it) } val opfResource = asset.container.get(opfPath) - val opfXmlDocument = opfResource.readAsXml() - .getOrElse { return Try.failure(it.toParserError()) } + ?: return Try.failure( + PublicationParser.Error.ReadError( + ReadError.Content( + MessageError("Missing OPF file.") + ) + ) + ) + val opfXmlDocument = opfResource + .use { it.decodeOrFail { readAsXml() } } + .getOrElse { return Try.failure(it) } val packageDocument = PackageDocument.parse(opfXmlDocument, opfPath, mediaTypeRetriever) - ?: return Try.failure(PublicationParser.Error.InvalidAsset("Invalid OPF file.")) + ?: return Try.failure( + PublicationParser.Error.ReadError( + ReadError.Content( + MessageError("Invalid OPF file.") + ) + ) + ) val manifest = ManifestAdapter( packageDocument = packageDocument, @@ -92,31 +110,43 @@ public class EpubParser( return Try.success(builder) } - private suspend fun getRootFilePath(container: Container): Try = - container + private suspend fun getRootFilePath(container: ClosedContainer): Try { + val containerXmlResource = container .get(Url("META-INF/container.xml")!!) - .use { it.readAsXml() } - .getOrElse { return Try.failure(it.toParserError()) } + ?: return Try.failure( + PublicationParser.Error.ReadError( + ReadError.Content("container.xml not found.") + ) + ) + + return containerXmlResource + .use { it.decodeOrFail { readAsXml() } } + .getOrElse { return Try.failure(it) } .getFirst("rootfiles", Namespaces.OPC) ?.getFirst("rootfile", Namespaces.OPC) ?.getAttr("full-path") ?.let { Url.fromEpubHref(it) } ?.let { Try.success(it) } - ?: Try.failure(PublicationParser.Error.InvalidAsset("Cannot successfully parse OPF.")) + ?: Try.failure( + PublicationParser.Error.ReadError( + ReadError.Content("Cannot successfully parse OPF.") + ) + ) + } - private suspend fun parseEncryptionData(container: Container): Map = + private suspend fun parseEncryptionData(container: ClosedContainer): Map = container.readAsXmlOrNull("META-INF/encryption.xml") ?.let { EncryptionParser.parse(it) } ?: emptyMap() - private suspend fun parseNavigationData(packageDocument: PackageDocument, container: Container): Map> = + private suspend fun parseNavigationData(packageDocument: PackageDocument, container: ClosedContainer): Map> = parseNavigationDocument(packageDocument, container) ?: parseNcx(packageDocument, container) ?: emptyMap() private suspend fun parseNavigationDocument( packageDocument: PackageDocument, - container: Container + container: ClosedContainer ): Map>? = packageDocument.manifest .firstOrNull { it.properties.contains(Vocabularies.ITEM + "nav") } @@ -126,7 +156,7 @@ public class EpubParser( } ?.takeUnless { it.isEmpty() } - private suspend fun parseNcx(packageDocument: PackageDocument, container: Container): Map>? { + private suspend fun parseNcx(packageDocument: PackageDocument, container: ClosedContainer): Map>? { val ncxItem = if (packageDocument.spine.toc != null) { packageDocument.manifest.firstOrNull { it.id == packageDocument.spine.toc } @@ -141,7 +171,7 @@ public class EpubParser( ?.takeUnless { it.isEmpty() } } - private suspend fun parseDisplayOptions(container: Container): Map { + private suspend fun parseDisplayOptions(container: ClosedContainer): Map { val displayOptionsXml = container.readAsXmlOrNull("META-INF/com.apple.ibooks.display-options.xml") ?: container.readAsXmlOrNull("META-INF/com.kobobooks.display-options.xml") @@ -155,4 +185,26 @@ public class EpubParser( } ?.toMap().orEmpty() } + + private suspend fun ResourceEntry.decodeOrFail( + decode: suspend Resource.() -> Try> + ): Try { + + return decode() + .mapFailure { + when (it) { + is DecoderError.DataAccess -> + PublicationParser.Error.ReadError(it.cause) + is DecoderError.DecodingError -> + PublicationParser.Error.ReadError( + ReadError.Content( + MessageError( + "Couldn't decode resource at $url", + it.cause + ) + ) + ) + } + } + } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt index 438bbb9138..7b13f1c5f2 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt @@ -17,10 +17,11 @@ import org.readium.r2.shared.publication.presentation.Presentation import org.readium.r2.shared.publication.presentation.presentation import org.readium.r2.shared.publication.services.PositionsService import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.archive.archive +import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.archive +import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.use /** @@ -35,7 +36,7 @@ import org.readium.r2.shared.util.use public class EpubPositionsService( private val readingOrder: List, private val presentation: Presentation, - private val container: Container, + private val container: ClosedContainer, private val reflowableStrategy: ReflowableStrategy ) : PositionsService { @@ -121,7 +122,9 @@ public class EpubPositionsService( if (presentation.layoutOf(link) == EpubLayout.FIXED) { createFixed(link, lastPositionOfPreviousResource) } else { - createReflowable(link, lastPositionOfPreviousResource, container) + container.get(link.url()) + ?.use { createReflowable(link, lastPositionOfPreviousResource, it) } + ?: emptyList() } positions.lastOrNull()?.locations?.position?.let { @@ -160,12 +163,11 @@ public class EpubPositionsService( ) ) - private suspend fun createReflowable(link: Link, startPosition: Int, container: Container): List { + private suspend fun createReflowable(link: Link, startPosition: Int, resource: Resource): List { val href = link.url() - val positionCount = container.get(href).use { resource -> + val positionCount = reflowableStrategy.positionCount(link, resource) - } return (1..positionCount).mapNotNull { position -> createLocator( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index 8f5b0c63f4..91f4ec3f09 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -11,7 +11,9 @@ import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Metadata import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.PerResourcePositionsService +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.streamer.extensions.guessTitle @@ -37,18 +39,22 @@ public class ImageParser : PublicationParser { val readingOrder = if (asset.mediaType.matches(MediaType.CBZ)) { - (asset.container.entries() ?: emptySet()) - .filter { !it.url.isHiddenOrThumbs && it.mediaType().getOrNull()?.isBitmap == true } - .sortedBy { it.url.toString() } + (asset.container.entries()) + .filter { !it.isHiddenOrThumbs && it.mediaType().getOrNull()?.isBitmap == true } + .sortedBy { it.toString() } } else { - listOfNotNull(asset.container.entries()?.firstOrNull()) + listOfNotNull(asset.container.entries().firstOrNull()) } .map { it.toLink() } .toMutableList() if (readingOrder.isEmpty()) { return Try.failure( - PublicationParser.Error.InvalidAsset("No bitmap found in the publication.") + PublicationParser.Error.ReadError( + ReadError.Content( + MessageError("No bitmap found in the publication.") + ) + ) ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index c1c903a435..fc1972a8ef 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -12,7 +12,9 @@ import org.readium.r2.shared.PdfSupport import org.readium.r2.shared.publication.* import org.readium.r2.shared.publication.services.InMemoryCacheService import org.readium.r2.shared.publication.services.InMemoryCoverService +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType @@ -20,7 +22,6 @@ import org.readium.r2.shared.util.pdf.PdfDocumentFactory import org.readium.r2.shared.util.pdf.toLinks import org.readium.r2.streamer.extensions.toLink import org.readium.r2.streamer.parser.PublicationParser -import org.readium.r2.streamer.parser.PublicationParser.Error.Companion.toParserError /** * Parses a PDF file into a Readium [Publication]. @@ -42,12 +43,18 @@ public class PdfParser( return Try.failure(PublicationParser.Error.UnsupportedFormat()) } - val resource = asset.container.entries()?.firstOrNull() + val resource = asset.container.entries() + .firstOrNull() + ?.let { asset.container.get(it) } ?: return Try.failure( - PublicationParser.Error.InvalidAsset("No PDF found in the publication.") + PublicationParser.Error.ReadError( + ReadError.Content( + MessageError("No PDF found in the publication.") + ) + ) ) val document = pdfFactory.open(resource, password = null) - .getOrElse { return Try.failure(it.toParserError()) } + .getOrElse { return Try.failure(PublicationParser.Error.ReadError(it)) } val tableOfContents = document.outline.toLinks(resource.url) val manifest = Manifest( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt index dc93cf3a42..8237393571 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt @@ -96,14 +96,18 @@ internal class LcpdfPositionsService( } } - private suspend fun openPdfAt(link: Link): PdfDocument? = - pdfFactory + private suspend fun openPdfAt(link: Link): PdfDocument? { + val resource = context.container.get(link.url()) + ?: return null + + return pdfFactory .cachedIn(context.services) - .open(context.container.get(link.url()), password = null) + .open(resource, password = null) .getOrElse { Timber.e(it) null } + } companion object { diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index 77b2b628d0..990a7ff990 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -7,23 +7,23 @@ package org.readium.r2.streamer.parser.readium import android.content.Context -import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.InMemoryCacheService import org.readium.r2.shared.publication.services.PerResourcePositionsService import org.readium.r2.shared.publication.services.cacheServiceFactory import org.readium.r2.shared.publication.services.locatorServiceFactory import org.readium.r2.shared.publication.services.positionsServiceFactory +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.readAsRwpm import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.pdf.PdfDocumentFactory -import org.readium.r2.shared.util.resource.readAsJson import org.readium.r2.streamer.parser.PublicationParser -import org.readium.r2.streamer.parser.PublicationParser.Error.Companion.toParserError import org.readium.r2.streamer.parser.audio.AudioLocatorService /** @@ -32,7 +32,6 @@ import org.readium.r2.streamer.parser.audio.AudioLocatorService public class ReadiumWebPubParser( private val context: Context? = null, private val pdfFactory: PdfDocumentFactory<*>?, - private val mediaTypeRetriever: MediaTypeRetriever ) : PublicationParser { override suspend fun parse( @@ -43,17 +42,32 @@ public class ReadiumWebPubParser( return Try.failure(PublicationParser.Error.UnsupportedFormat()) } - val manifestJson = asset.container + val manifest = asset.container .get(Url("manifest.json")!!) - .readAsJson() - .getOrElse { return Try.failure(it.toParserError()) } - - val manifest = Manifest.fromJSON( - manifestJson, - mediaTypeRetriever = mediaTypeRetriever - ) - ?: return Try.failure( - PublicationParser.Error.InvalidAsset("Failed to parse the RWPM Manifest") + ?.readAsRwpm() + ?.getOrElse { + when (it) { + is DecoderError.DataAccess -> + return Try.failure( + PublicationParser.Error.ReadError( + ReadError.Content(it.cause) + ) + ) + is DecoderError.DecodingError -> + return Try.failure( + PublicationParser.Error.ReadError( + ReadError.Content( + MessageError("Failed to parse the RWPM Manifest.") + ) + ) + ) + } + } ?: return Try.failure( + PublicationParser.Error.ReadError( + ReadError.Content( + MessageError("Missing manifest.") + ) + ) ) // Checks the requirements from the LCPDF specification. @@ -62,7 +76,11 @@ public class ReadiumWebPubParser( if (asset.mediaType == MediaType.LCP_PROTECTED_PDF && (readingOrder.isEmpty() || !readingOrder.all { MediaType.PDF.matches(it.mediaType) }) ) { - return Try.failure(PublicationParser.Error.InvalidAsset("Invalid LCP Protected PDF.")) + return Try.failure( + PublicationParser.Error.ReadError( + ReadError.Content("Invalid LCP Protected PDF.") + ) + ) } val servicesBuilder = Publication.ServicesBuilder().apply { diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/ContainerEntryTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/ContainerEntryTest.kt index bec497f152..d29069a6cd 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/ContainerEntryTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/ContainerEntryTest.kt @@ -17,7 +17,6 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry import org.robolectric.RobolectricTestRunner @@ -29,7 +28,7 @@ class ContainerEntryTest { override val source: AbsoluteUrl? = null override suspend fun mediaType(): ResourceTry = throw NotImplementedError() - override suspend fun properties(): ResourceTry = + override suspend fun properties(): ResourceTry = throw NotImplementedError() override suspend fun length(): ResourceTry = throw NotImplementedError() diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt index d6575f495f..be53686d6d 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt @@ -21,12 +21,11 @@ import org.readium.r2.shared.publication.presentation.Presentation 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.archive.ArchiveProperties +import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ArchiveProperties import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry -import org.readium.r2.shared.util.resource.archive import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -503,7 +502,7 @@ class EpubPositionsServiceTest { override suspend fun mediaType(): ResourceTry = Try.success(item.link.mediaType ?: MediaType.BINARY) - override suspend fun properties(): ResourceTry = + override suspend fun properties(): ResourceTry = Try.success(item.resourceProperties) override suspend fun length() = Try.success(item.length) @@ -552,7 +551,7 @@ class EpubPositionsServiceTest { ) ) - val resourceProperties: Resource.Properties = Resource.Properties { + val resourceProperties: Properties = Properties { if (archiveEntryLength != null) { archive = ArchiveProperties( entryLength = archiveEntryLength, diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index e65fdddf78..cfc0a11eeb 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -19,10 +19,10 @@ import org.junit.runner.RunWith import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.firstWithRel import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.archive.FileZipArchiveProvider +import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever -import org.readium.r2.shared.util.resource.FileResource -import org.readium.r2.shared.util.resource.FileZipArchiveProvider import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toUrl import org.readium.r2.streamer.parseBlocking @@ -36,7 +36,7 @@ class ImageParserTest { private val cbzAsset = runBlocking { val file = fileForResource("futuristic_tales.cbz") - val resource = FileResource(file, mediaType = MediaType.CBZ) + val resource = FileBlob(file, mediaType = MediaType.CBZ) val archive = FileZipArchiveProvider(MediaTypeRetriever()).create( resource, password = null @@ -46,7 +46,7 @@ class ImageParserTest { private val jpgAsset = runBlocking { val file = fileForResource("futuristic_tales.jpg") - val resource = FileResource(file, mediaType = MediaType.JPEG) + val resource = FileBlob(file, mediaType = MediaType.JPEG) PublicationParser.Asset( mediaType = MediaType.JPEG, ResourceContainer(file.toUrl(), resource) diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 19d44ad6b2..873849eba7 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -17,14 +17,14 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetriever import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.CompositeResourceFactory +import org.readium.r2.shared.util.asset.HttpResourceFactory +import org.readium.r2.shared.util.data.FileResourceFactory import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager import org.readium.r2.shared.util.http.DefaultHttpClient -import org.readium.r2.shared.util.http.HttpResourceFactory import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaTypeRetriever -import org.readium.r2.shared.util.resource.CompositeResourceFactory import org.readium.r2.shared.util.resource.ContentResourceFactory -import org.readium.r2.shared.util.resource.FileResourceFactory import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider import org.readium.r2.streamer.PublicationFactory diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index 00c1d22b09..3739acfb67 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -18,8 +18,8 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.shared.util.toUrl import org.readium.r2.streamer.PublicationFactory import org.readium.r2.testapp.data.BookRepository @@ -143,7 +143,7 @@ class Bookshelf( coverStorage.storeCover(publication, coverUrl) .getOrElse { return Try.failure( - ImportError.ResourceError(ResourceError.Filesystem(it)) + ImportError.ResourceError(ReadError.Filesystem(it)) ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt index e90be10a37..504d50ae22 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt @@ -8,7 +8,7 @@ package org.readium.r2.testapp.domain import androidx.annotation.StringRes import org.readium.r2.shared.UserException -import org.readium.r2.shared.util.asset.AssetError +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.testapp.R @@ -44,7 +44,7 @@ sealed class ImportError( } class ResourceError( - val error: org.readium.r2.shared.util.resource.ResourceError + val error: ReadError ) : ImportError(R.string.import_publication_unexpected_io_exception) class DownloadFailed( diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index d1c60d7dfd..1d0ef58018 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -9,7 +9,6 @@ package org.readium.r2.testapp.domain import androidx.annotation.StringRes import org.readium.r2.shared.UserException import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.testapp.R diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index b6dc3540d5..fa522c1deb 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -24,11 +24,11 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ResourceError import org.readium.r2.testapp.data.DownloadRepository import org.readium.r2.testapp.utils.extensions.copyToTempFile import org.readium.r2.testapp.utils.extensions.moveTo @@ -123,7 +123,7 @@ class LocalPublicationRetriever( coroutineScope.launch { val tempFile = uri.copyToTempFile(context, storageDir) .getOrElse { - listener.onError(ImportError.ResourceError(ResourceError.Filesystem(it))) + listener.onError(ImportError.ResourceError(ReadError.Filesystem(it))) return@launch } @@ -178,7 +178,7 @@ class LocalPublicationRetriever( } catch (e: Exception) { Timber.d(e) tryOrNull { libraryFile.delete() } - listener.onError(ImportError.ResourceError(ResourceError.Filesystem(e))) + listener.onError(ImportError.ResourceError(ReadError.Filesystem(e))) return } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt index 1790de6dc7..e88ff1434b 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt @@ -24,7 +24,6 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.allAreHtml import org.readium.r2.shared.publication.services.isRestricted import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.AssetError import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.getOrElse import org.readium.r2.testapp.Readium diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt index a9cf438d43..1f2d3af55f 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt @@ -34,7 +34,7 @@ import org.readium.r2.shared.publication.services.search.search 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.resource.ResourceError +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.testapp.Application import org.readium.r2.testapp.data.BookRepository import org.readium.r2.testapp.data.model.Highlight @@ -270,9 +270,9 @@ class ReaderViewModel( // Navigator.Listener - override fun onResourceLoadFailed(href: Url, error: ResourceError) { + override fun onResourceLoadFailed(href: Url, error: ReadError) { val message = when (error) { - is ResourceError.OutOfMemory -> "The resource is too large to be rendered on this device: $href" + is ReadError.OutOfMemory -> "The resource is too large to be rendered on this device: $href" else -> "Failed to render the resource: $href" } activityChannel.send(ActivityCommand.ToastError(UserException(message, error))) From 524d9ccf4050fdf5d547f1f298af08376e0513ff Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 15 Nov 2023 10:55:26 +0100 Subject: [PATCH 08/86] Various --- .../exoplayer/audio/ExoPlayerDataSource.kt | 9 +- .../adapter/pdfium/document/PdfiumDocument.kt | 25 +++- .../navigator/PdfiumDocumentFragment.kt | 4 +- .../pspdfkit/document/PsPdfKitDocument.kt | 5 +- .../navigator/PsPdfKitDocumentFragment.kt | 5 +- .../readium/r2/lcp/LcpContentProtection.kt | 2 +- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 6 +- .../org/readium/r2/lcp/LcpDecryptorTest.kt | 12 +- .../readium/r2/lcp/LcpPublicationRetriever.kt | 3 +- .../java/org/readium/r2/lcp/LcpService.kt | 3 +- .../container/ContainerLicenseContainer.kt | 20 +-- .../container/ContentZipLicenseContainer.kt | 5 +- .../lcp/license/container/LicenseContainer.kt | 5 +- .../r2/lcp/license/model/LicenseDocument.kt | 3 +- .../readium/r2/lcp/service/LicensesService.kt | 8 +- .../navigator/media2/ExoPlayerDataSource.kt | 9 +- .../readium/r2/navigator/R2BasicWebView.kt | 2 +- .../navigator/audio/PublicationDataSource.kt | 9 +- .../r2/navigator/epub/WebViewServer.kt | 44 ++++-- .../r2/navigator/media/ExoMediaPlayer.kt | 4 +- .../r2/navigator/pager/R2CbzPageFragment.kt | 4 +- .../navigator/media/audio/AudioNavigator.kt | 2 +- .../media/tts/session/TtsSessionAdapter.kt | 16 +- .../java/org/readium/r2/opds/OPDS1Parser.kt | 20 +-- .../java/org/readium/r2/opds/OPDS2Parser.kt | 12 +- .../r2/shared/publication/Publication.kt | 52 ++++--- .../services/ContentProtectionService.kt | 139 ++++++++++-------- .../publication/services/CoverService.kt | 113 +++++++++++--- .../publication/services/PositionsService.kt | 64 +++++--- .../services/search/SearchService.kt | 2 +- .../r2/shared/util/archive/ArchiveProvider.kt | 4 +- .../util/archive/FileZipArchiveProvider.kt | 6 +- .../r2/shared/util/archive/ZipContainer.kt | 2 +- .../org/readium/r2/shared/util/data/Blob.kt | 7 +- .../r2/shared/util/data/BlobInputStream.kt | 13 +- .../readium/r2/shared/util/data/Container.kt | 2 +- .../r2/shared/util/data/ContentBlob.kt | 3 +- .../readium/r2/shared/util/data/Decoding.kt | 36 ++--- .../readium/r2/shared/util/data/FileBlob.kt | 3 +- .../shared/util/{ => data}/FilesystemError.kt | 5 +- .../readium/r2/shared/util/data/HttpError.kt | 118 +++++++++++++++ .../r2/shared/util/data/InMemoryBlob.kt | 57 +++++++ .../readium/r2/shared/util/data/ReadError.kt | 17 ++- .../shared/util/downloads/DownloadManager.kt | 6 +- .../android/AndroidDownloadManager.kt | 11 +- .../foreground/ForegroundDownloadManager.kt | 6 +- .../r2/shared/util/http/DefaultHttpClient.kt | 56 ++++--- .../readium/r2/shared/util/http/HttpClient.kt | 16 +- .../readium/r2/shared/util/http/HttpError.kt | 127 ---------------- .../r2/shared/util/http/HttpRequest.kt | 14 +- .../r2/shared/util/http/HttpResource.kt | 28 +--- .../util/mediatype/DefaultMediaTypeSniffer.kt | 3 +- .../util/mediatype/MediaTypeRetriever.kt | 3 +- .../shared/util/mediatype/MediaTypeSniffer.kt | 24 +-- .../util/resource/BlobResourceAdapters.kt | 8 +- .../r2/shared/util/resource/BytesResource.kt | 109 -------------- .../r2/shared/util/resource/Resource.kt | 17 +-- .../r2/shared/util/resource/StringResource.kt | 47 ++++++ .../{DatasourceChannel.kt => BlobChannel.kt} | 8 +- .../r2/shared/util/zip/ChannelZipContainer.kt | 8 +- .../util/zip/StreamingZipArchiveProvider.kt | 28 ++-- .../r2/shared/publication/PublicationTest.kt | 4 +- .../publication/protection/TestContainer.kt | 6 +- .../services/PositionsServiceTest.kt | 2 +- .../HtmlResourceContentIteratorTest.kt | 4 +- .../r2/streamer/extensions/Container.kt | 20 +-- .../readium/r2/streamer/extensions/Link.kt | 18 ++- .../r2/streamer/parser/audio/AudioParser.kt | 4 +- .../r2/streamer/parser/epub/EpubParser.kt | 13 +- .../r2/streamer/parser/image/ImageParser.kt | 13 +- .../parser/readium/ReadiumWebPubParser.kt | 16 +- .../readium/r2/testapp/domain/CoverStorage.kt | 2 +- 72 files changed, 838 insertions(+), 663 deletions(-) rename readium/shared/src/main/java/org/readium/r2/shared/util/{ => data}/FilesystemError.kt (87%) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpError.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/data/InMemoryBlob.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/zip/{DatasourceChannel.kt => BlobChannel.kt} (94%) diff --git a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt index 512af8b095..a6a779950e 100644 --- a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt @@ -63,16 +63,15 @@ internal class ExoPlayerDataSource internal constructor( private var openedResource: OpenedResource? = null override fun open(dataSpec: DataSpec): Long { - val link = dataSpec.uri.toUrl() + val resource = dataSpec.uri.toUrl() ?.let { publication.linkWithHref(it) } + ?.let { publication.get(it) } + // Significantly improves performances, in particular with deflated ZIP entries. + ?.buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) ?: throw ExoPlayerDataSourceException.NotFound( "Can't find a [Link] for URI: ${dataSpec.uri}. Make sure you only request resources declared in the manifest." ) - val resource = publication.get(link) - // Significantly improves performances, in particular with deflated ZIP entries. - .buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) - openedResource = OpenedResource( resource = resource, uri = dataSpec.uri, diff --git a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt index 1f12e0ff78..822d1f4e98 100644 --- a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt +++ b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt @@ -18,14 +18,15 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.extensions.md5 import org.readium.r2.shared.extensions.tryOrNull -import org.readium.r2.shared.util.MessageError -import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.data.unwrapReadException +import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry -import org.readium.r2.shared.util.resource.decode import org.readium.r2.shared.util.use import timber.log.Timber @@ -106,10 +107,20 @@ public class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory = use { - it.decode( - { bytes -> core.fromBytes(bytes, password) }, - { MessageError("Pdfium could not read data.", ThrowableError(it)) } - ) + it.read() + .flatMap { bytes -> + try { + Try.success( + core.fromBytes(bytes, password) + ) + } catch (e: Exception) { + val error = when (val exception = e.unwrapReadException()) { + is ReadException -> exception.error + else -> ReadError.Content("Pdfium could not read data.") + } + Try.failure(error) + } + } } private fun PdfiumCore.fromFile(file: File, password: String?): PdfiumDocument = diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt index 87b8ab0af6..1e34caccd8 100644 --- a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt @@ -27,6 +27,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.SingleJob import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.e import org.readium.r2.shared.util.getOrElse import timber.log.Timber @@ -69,10 +70,11 @@ public class PdfiumDocumentFragment internal constructor( val context = context?.applicationContext ?: return resetJob.launch { + val resource = requireNotNull(publication.get(href)) val document = PdfiumDocumentFactory(context) // PDFium crashes when reusing the same PdfDocument, so we must not cache it. // .cachedIn(publication) - .open(publication.get(href), null) + .open(resource, null) .getOrElse { error -> Timber.e(error) listener?.onResourceLoadFailed(href, error) diff --git a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt index 6a519827a0..c416ea0ba8 100644 --- a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt @@ -20,11 +20,12 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.publication.ReadingProgression +import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry import timber.log.Timber @@ -44,7 +45,7 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory = - lcpService.isLcpProtected(asset) + Try.success(lcpService.isLcpProtected(asset)) override suspend fun open( asset: Asset, diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 133c105356..eaca6f07ce 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -59,7 +59,11 @@ internal class LcpDecryptor( when { license == null -> FailureResource( - ReadError.Content() + ReadError.Content( + MessageError( + "Cannot decipher content because the publication is locked." + ) + ) ) encryption.isDeflated || !encryption.isCbcEncrypted -> FullLcpResource( diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt index 7fd861c18b..ab23d09f72 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt @@ -39,7 +39,7 @@ internal suspend fun checkResourcesAreReadableInOneBlock(publication: Publicatio (publication.readingOrder + publication.resources) .forEach { link -> Timber.d("attempting to read ${link.href} in one block") - publication.get(link).use { resource -> + publication.get(link)!!.use { resource -> val bytes = resource.read() check(bytes.isSuccess) { "failed to read ${link.href} in one block" } } @@ -51,8 +51,8 @@ internal suspend fun checkLengthComputationIsCorrect(publication: Publication) { (publication.readingOrder + publication.resources) .forEach { link -> - val trueLength = publication.get(link).use { it.read().assertSuccess().size.toLong() } - publication.get(link).use { resource -> + val trueLength = publication.get(link)!!.use { it.read().assertSuccess().size.toLong() } + publication.get(link)!!.use { resource -> resource.length() .onFailure { throw IllegalStateException( @@ -72,10 +72,10 @@ internal suspend fun checkAllResourcesAreReadableByChunks(publication: Publicati (publication.readingOrder + publication.resources) .forEach { link -> Timber.d("attempting to read ${link.href} by chunks ") - val groundTruth = publication.get(link).use { it.read() }.assertSuccess() + val groundTruth = publication.get(link)!!.use { it.read() }.assertSuccess() for (chunkSize in listOf(4096L, 2050L)) { publication.get(link).use { resource -> - resource.readByChunks(chunkSize, groundTruth).onFailure { + resource!!.readByChunks(chunkSize, groundTruth).onFailure { throw IllegalStateException( "failed to read ${link.href} by chunks of size $chunkSize", it @@ -92,7 +92,7 @@ internal suspend fun checkExceedingRangesAreAllowed(publication: Publication) { (publication.readingOrder + publication.resources) .forEach { link -> publication.get(link).use { resource -> - val length = resource.length().assertSuccess() + val length = resource!!.length().assertSuccess() val fullTruth = resource.read().assertSuccess() for ( range in listOf( diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt index 8582f96031..d09f420d82 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt @@ -13,6 +13,7 @@ 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.AbsoluteUrl import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.mediatype.FormatRegistry @@ -153,7 +154,7 @@ public class LcpPublicationRetriever( private fun fetchPublication( license: LicenseDocument ): RequestId { - val url = license.publicationLink.url() + val url = license.publicationLink.url() as AbsoluteUrl val requestId = downloadManager.submit( request = DownloadManager.Request( diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt index 22aec3ce0f..652b493063 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt @@ -30,7 +30,6 @@ import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever @@ -48,7 +47,7 @@ public interface LcpService { /** * Returns if the asset is a LCP license document or a publication protected by LCP. */ - public suspend fun isLcpProtected(asset: Asset): Try + public suspend fun isLcpProtected(asset: Asset): Boolean /** * Acquires a protected publication from a standalone LCPL's bytes. diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt index 2965542bd9..e533706c4a 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt @@ -9,30 +9,26 @@ package org.readium.r2.lcp.license.container import kotlinx.coroutines.runBlocking import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.getOrThrow -import org.readium.r2.shared.util.resource.Container +import org.readium.r2.shared.util.resource.ResourceEntry /** * Access to a License Document stored in a read-only container. */ internal class ContainerLicenseContainer( - private val container: Container, + private val container: ClosedContainer, private val entryUrl: Url ) : LicenseContainer { override fun read(): ByteArray { return runBlocking { - container - .get(entryUrl) - .read() + val resource = container.get(entryUrl) + ?: throw LcpException.Container.FileNotFound(entryUrl) + + resource.read() .mapFailure { - when (it) { - is ReadError.NotFound -> - LcpException.Container.FileNotFound(entryUrl) - else -> - LcpException.Container.ReadFailed(entryUrl) - } + LcpException.Container.ReadFailed(entryUrl) } .getOrThrow() } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt index 3279c2022d..1fd8177eeb 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt @@ -17,12 +17,13 @@ import java.util.zip.ZipFile import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.Container +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.toUri internal class ContentZipLicenseContainer( context: Context, - private val container: Container, + private val container: ClosedContainer, private val pathInZip: Url ) : LicenseContainer by ContainerLicenseContainer(container, pathInZip), WritableLicenseContainer { diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt index af7f1e1ef3..66665a21c2 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt @@ -15,9 +15,10 @@ import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceEntry private val LICENSE_IN_EPUB = Url("META-INF/license.lcpl")!! private val LICENSE_IN_RPF = Url("license.lcpl")!! @@ -72,7 +73,7 @@ internal fun createLicenseContainer( internal fun createLicenseContainer( context: Context, - container: Container, + container: ClosedContainer, mediaType: MediaType ): LicenseContainer { val licensePath = when (mediaType) { diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/LicenseDocument.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/LicenseDocument.kt index 02e99d6952..eb898ded7f 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/LicenseDocument.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/LicenseDocument.kt @@ -22,6 +22,7 @@ import org.readium.r2.lcp.license.model.components.lcp.User import org.readium.r2.lcp.service.URLParameters import org.readium.r2.shared.extensions.iso8601ToDate import org.readium.r2.shared.extensions.optNullableString +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType @@ -73,7 +74,7 @@ public class LicenseDocument internal constructor(public val json: JSONObject) { // Check that the acquisition link has a valid URL. try { - link(Rel.Publication)!!.url() + link(Rel.Publication)!!.url() as AbsoluteUrl } catch (e: Exception) { throw LcpException.Parsing.Url(rel = LicenseDocument.Rel.Publication.value) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index 49e37fa789..8ee6793982 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -11,7 +11,6 @@ package org.readium.r2.lcp.service import android.content.Context import java.io.File -import java.lang.Error import kotlin.coroutines.resume import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -38,7 +37,6 @@ import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry @@ -64,11 +62,11 @@ internal class LicensesService( return isLcpProtected(asset) } - override suspend fun isLcpProtected(asset: Asset): Try = + override suspend fun isLcpProtected(asset: Asset): Boolean = tryOr(false) { when (asset) { is Asset.Resource -> - Try.success(asset.mediaType == MediaType.LCP_LICENSE_DOCUMENT) + asset.mediaType == MediaType.LCP_LICENSE_DOCUMENT is Asset.Container -> { createLicenseContainer(context, asset.container, asset.mediaType).read() true @@ -79,7 +77,7 @@ internal class LicensesService( override fun contentProtection( authentication: LcpAuthenticating ): ContentProtection = - LcpContentProtection(this, authentication, assetRetriever) + LcpContentProtection(this, authentication, assetRetriever, mediaTypeRetriever) override fun publicationRetriever(): LcpPublicationRetriever { return LcpPublicationRetriever( diff --git a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt index 08f544e012..29ea63f923 100644 --- a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt +++ b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt @@ -65,16 +65,15 @@ public class ExoPlayerDataSource internal constructor(private val publication: P private var openedResource: OpenedResource? = null override fun open(dataSpec: DataSpec): Long { - val link = dataSpec.uri.toUrl() + val resource = dataSpec.uri.toUrl() ?.let { publication.linkWithHref(it) } + ?.let { publication.get(it) } + // Significantly improves performances, in particular with deflated ZIP entries. + ?.buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) ?: throw ExoPlayerDataSourceException.NotFound( "Can't find a [Link] for URI: ${dataSpec.uri}. Make sure you only request resources declared in the manifest." ) - val resource = publication.get(link) - // Significantly improves performances, in particular with deflated ZIP entries. - .buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) - openedResource = OpenedResource( resource = resource, uri = dataSpec.uri, diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt index 9ad58800ac..bc3569f9e0 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt @@ -48,8 +48,8 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.data.readAsString import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.readAsString import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.use import timber.log.Timber diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt index f4ddbc6067..13b2a7c0fb 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt @@ -65,16 +65,15 @@ internal class PublicationDataSource(private val publication: Publication) : Bas private var openedResource: OpenedResource? = null override fun open(dataSpec: DataSpec): Long { - val link = dataSpec.uri.toUrl() + val resource = dataSpec.uri.toUrl() ?.let { publication.linkWithHref(it) } + ?.let { publication.get(it) } + // Significantly improves performances, in particular with deflated ZIP entries. + ?.buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) ?: throw PublicationDataSourceException.NotFound( "Can't find a [Link] for URI: ${dataSpec.uri}. Make sure you only request resources declared in the manifest." ) - val resource = publication.get(link) - // Significantly improves performances, in particular with deflated ZIP entries. - .buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) - openedResource = OpenedResource( resource = resource, uri = dataSpec.uri, diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt index 54ff53627a..8c006aa6f0 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt @@ -11,19 +11,25 @@ import android.os.PatternMatcher import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import androidx.webkit.WebViewAssetLoader +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.readium.r2.navigator.epub.css.ReadiumCss import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication 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.data.AccessException import org.readium.r2.shared.util.data.BlobInputStream import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.http.HttpHeaders import org.readium.r2.shared.util.http.HttpRange +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.StringResource +import org.readium.r2.shared.util.resource.fallback /** * Serves the publication resources and application assets in the EPUB navigator web views. @@ -84,13 +90,21 @@ internal class WebViewServer( ?: Link(href = href) // Drop anchor because it is meant to be interpreted by the client. - val linkWithoutAnchor = link.copy( - href = Href(href.removeFragment()) - ) + val urlWithoutAnchor = href.removeFragment() + + var resource = publication + .get(urlWithoutAnchor) + ?.fallback { + onResourceLoadFailed(urlWithoutAnchor, it) + errorResource(urlWithoutAnchor, it) + } ?: run { + val error = ReadError.Content( + "Resource not found at $urlWithoutAnchor in publication." + ) + onResourceLoadFailed(urlWithoutAnchor, error) + errorResource(urlWithoutAnchor, error) + } - var resource = publication.get(linkWithoutAnchor) - // FIXME: report loading errors through Navigator.Listener.onResourceLoadingFailed - // .fallback { errorResource(link, error = it) } if (link.mediaType?.isHtml == true) { resource = resource.injectHtml( publication, @@ -111,10 +125,10 @@ internal class WebViewServer( 200, "OK", headers, - BlobInputStream(resource, ::AccessException) + BlobInputStream(resource, ::ReadException) ) } else { // Byte range request - val stream = BlobInputStream(resource, ::AccessException) + val stream = BlobInputStream(resource, ::ReadException) val length = stream.available() val longRange = range.toLongRange(length.toLong()) headers["Content-Range"] = "bytes ${longRange.first}-${longRange.last}/$length" @@ -131,6 +145,18 @@ internal class WebViewServer( ) } } + private fun errorResource(url: Url, error: ReadError): Resource = + StringResource(mediaType = MediaType.XHTML) { + withContext(Dispatchers.IO) { + Try.success( + application.assets + .open("readium/error.xhtml").bufferedReader() + .use { it.readText() } + .replace("\${error}", error.message) + .replace("\${href}", url.toString()) + ) + } + } private fun isServedAsset(path: String): Boolean = servedAssetPatterns.any { it.match(path) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt index c0553fc477..590c26c19c 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt @@ -54,9 +54,9 @@ import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationId import org.readium.r2.shared.publication.indexOfFirstWithHref -import org.readium.r2.shared.util.NetworkError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.HttpError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.toUri import timber.log.Timber @@ -205,7 +205,7 @@ public class ExoMediaPlayer( var resourceError: ReadError? = error.asInstance() if (resourceError == null && (error.cause as? HttpDataSource.HttpDataSourceException)?.cause is UnknownHostException) { resourceError = ReadError.Network( - NetworkError.Offline(ThrowableError(error.cause!!)) + HttpError.UnreachableHost(ThrowableError(error.cause!!)) ) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2CbzPageFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2CbzPageFragment.kt index d058449e72..218c352289 100755 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2CbzPageFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2CbzPageFragment.kt @@ -61,8 +61,8 @@ internal class R2CbzPageFragment( launch { publication.get(link) - .read() - .getOrNull() + ?.read() + ?.getOrNull() ?.let { BitmapFactory.decodeByteArray(it, 0, it.size) } ?.let { photoView.setImageBitmap(it) } } diff --git a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigator.kt b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigator.kt index 593c600b40..6272f0f922 100644 --- a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigator.kt +++ b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigator.kt @@ -87,7 +87,7 @@ public class AudioNavigator= Build.VERSION_CODES.M) { - val resource = publication.get(link) + val resource = requireNotNull(publication.get(link)) val metadataRetriever = MetadataRetriever(resource) duration = metadataRetriever.duration() metadataRetriever.close() diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt index 7dd9477df7..874d708562 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt @@ -44,6 +44,7 @@ import org.readium.navigator.media.tts.TtsEngine import org.readium.navigator.media.tts.TtsPlayer import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.data.HttpError import org.readium.r2.shared.util.data.ReadError /** @@ -924,12 +925,17 @@ internal class TtsSessionAdapter( } is TtsPlayer.State.Error.ContentError -> { val errorCode = when (error) { - is ReadError.NotFound -> - ERROR_CODE_IO_BAD_HTTP_STATUS - is ReadError.Forbidden -> - ERROR_CODE_DRM_DISALLOWED_OPERATION is ReadError.Network -> - ERROR_CODE_IO_NETWORK_CONNECTION_FAILED + when (error.cause) { + is HttpError.Response -> + ERROR_CODE_IO_BAD_HTTP_STATUS + is HttpError.Timeout -> + ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT + is HttpError.UnreachableHost -> + ERROR_CODE_IO_NETWORK_CONNECTION_FAILED + else -> ERROR_CODE_UNSPECIFIED + } + else -> ERROR_CODE_UNSPECIFIED } diff --git a/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt b/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt index 294c9d45d6..39e62c935c 100644 --- a/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt +++ b/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt @@ -15,6 +15,7 @@ import org.readium.r2.shared.extensions.toMap import org.readium.r2.shared.opds.* import org.readium.r2.shared.publication.* import org.readium.r2.shared.toJSON +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url @@ -52,15 +53,16 @@ public class OPDS1Parser { url: String, client: HttpClient = DefaultHttpClient(MediaTypeRetriever()) ): Try = - parseRequest(HttpRequest(url), client) + AbsoluteUrl(url) + ?.let { parseRequest(HttpRequest(it), client) } + ?: run { Try.failure(Exception("Not an absolute URL.")) } public suspend fun parseRequest( request: HttpRequest, client: HttpClient = DefaultHttpClient(MediaTypeRetriever()) ): Try { return client.fetchWithDecoder(request) { - val url = Url(request.url) ?: throw Exception("Invalid URL") - this.parse(it.body, url) + this.parse(it.body, request.url) }.mapFailure { ErrorException(it) } } @@ -197,7 +199,7 @@ public class OPDS1Parser { feed: Feed, client: HttpClient = DefaultHttpClient(MediaTypeRetriever()) ): Try { - var openSearchURL: String? = null + var openSearchURL: Href? = null var selfMimeType: MediaType? = null for (link in feed.links) { @@ -206,15 +208,15 @@ public class OPDS1Parser { selfMimeType = link.mediaType } } else if (link.rels.contains("search")) { - openSearchURL = link.href.toString() + openSearchURL = link.href } } - val unwrappedURL = openSearchURL?.let { - return@let it - } + val unwrappedURL = openSearchURL + ?.let { it.resolve() as? AbsoluteUrl } + ?: return Try.success(null) - return client.fetchWithDecoder(HttpRequest(unwrappedURL.toString())) { + return client.fetchWithDecoder(HttpRequest(unwrappedURL)) { val document = XmlParser().parse(it.body.inputStream()) val urls = document.get("Url", Namespaces.Search) diff --git a/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt b/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt index 92d5c7df18..5afea4d5fe 100644 --- a/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt +++ b/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt @@ -24,6 +24,7 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.normalizeHrefsToBase +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url @@ -49,15 +50,16 @@ public class OPDS2Parser { url: String, client: HttpClient = DefaultHttpClient(MediaTypeRetriever()) ): Try = - parseRequest(HttpRequest(url), client) + AbsoluteUrl(url) + ?.let { parseRequest(HttpRequest(it), client) } + ?: run { Try.failure(Exception("Not an absolute URL.")) } public suspend fun parseRequest( request: HttpRequest, client: HttpClient = DefaultHttpClient(MediaTypeRetriever()) ): Try { return client.fetchWithDecoder(request) { - val url = Url(request.url) ?: throw Exception("Invalid URL") - this.parse(it.body, url) + this.parse(it.body, request.url) }.mapFailure { ErrorException(it) } } @@ -273,13 +275,13 @@ public class OPDS2Parser { } private fun parsePublication(json: JSONObject, baseUrl: Url): Publication? = - Manifest.fromJSON(json, mediaTypeSniffer = mediaTypeRetriever) + Manifest.fromJSON(json) // Self link takes precedence over the given `baseUrl`. ?.let { it.normalizeHrefsToBase(it.linkWithRel("self")?.href?.resolve() ?: baseUrl) } ?.let { Publication(it) } private fun parseLink(json: JSONObject, baseUrl: Url): Link? = - Link.fromJSON(json, mediaTypeRetriever) + Link.fromJSON(json) ?.normalizeHrefsToBase(baseUrl) public var mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index 31ee97fd88..c147f9f264 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -22,15 +22,22 @@ import org.readium.r2.shared.publication.services.CacheService import org.readium.r2.shared.publication.services.ContentProtectionService import org.readium.r2.shared.publication.services.CoverService import org.readium.r2.shared.publication.services.DefaultLocatorService +import org.readium.r2.shared.publication.services.ExternalCoverService import org.readium.r2.shared.publication.services.LocatorService import org.readium.r2.shared.publication.services.PositionsService +import org.readium.r2.shared.publication.services.ResourceCoverService import org.readium.r2.shared.publication.services.WebPositionsService import org.readium.r2.shared.publication.services.content.ContentService import org.readium.r2.shared.publication.services.search.SearchService import org.readium.r2.shared.util.Closeable +import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.data.EmptyContainer +import org.readium.r2.shared.util.data.HttpError +import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpRequest +import org.readium.r2.shared.util.http.HttpStreamResponse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceEntry @@ -61,9 +68,10 @@ public typealias PublicationContainer = ClosedContainer * Publication.Service attached to this Publication. */ public class Publication( - manifest: Manifest, + public val manifest: Manifest, private val container: PublicationContainer = EmptyContainer(), private val servicesBuilder: ServicesBuilder = ServicesBuilder(), + private val httpClient: HttpClient? = null, @Deprecated( "Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR @@ -76,14 +84,12 @@ public class Publication( public var cssStyle: String? = null ) : PublicationServicesHolder { - public val manifest: Manifest - private val services = ListPublicationServicesHolder() init { - services.services = servicesBuilder.build(Service.Context(manifest, container, services)) - this.manifest = manifest.copy( - links = manifest.links + services.services.map(Service::links).flatten() + services.services = servicesBuilder.build( + context = Service.Context(manifest, container, services), + httpClient = httpClient ) } @@ -205,8 +211,6 @@ public class Publication( get(href, linkWithHref(href)?.mediaType) private fun get(href: Url, mediaType: MediaType?): Resource? { - services.services.forEach { service -> service.get(href)?.let { return it } } - val entry = container.get(href) ?: container.get(href.removeQuery().removeFragment()) // Try again after removing query and fragment. ?: return null @@ -358,6 +362,14 @@ public class Publication( public val services: PublicationServicesHolder ) + /** + * Closes any opened file handles, removes temporary files, etc. + */ + override fun close() {} + } + + public interface WebService : Service { + /** * Links which will be added to [Publication.links]. * It can be used to expose a web API for the service, through [Publication.get]. @@ -375,7 +387,7 @@ public class Publication( * ) * ``` */ - public val links: List get() = emptyList() + public val links: List /** * A service can return a Resource to: @@ -392,12 +404,7 @@ public class Publication( * @return The [Resource] containing the response, or null if the service doesn't * recognize this request. */ - public fun get(href: Url): Resource? = null - - /** - * Closes any opened file handles, removes temporary files, etc. - */ - override fun close() {} + public suspend fun handle(request: HttpRequest): Try? } /** @@ -432,7 +439,7 @@ public class Publication( ) /** Builds the actual list of publication services to use in a Publication. */ - public fun build(context: Service.Context): List { + public fun build(context: Service.Context, httpClient: HttpClient?): List { val serviceFactories = buildMap { putAll(this@ServicesBuilder.serviceFactories) @@ -444,10 +451,19 @@ public class Publication( put(LocatorService::class.java.simpleName, factory) } - if (!containsKey(PositionsService::class.java.simpleName)) { - val factory = WebPositionsService.createFactory() + if (httpClient != null && !containsKey(PositionsService::class.java.simpleName)) { + val factory = WebPositionsService.createFactory(httpClient) put(PositionsService::class.java.simpleName, factory) } + + if (!containsKey(CoverService::class.java.simpleName)) { + val factory = { context: Service.Context -> + ResourceCoverService.createFactory()(context) + ?: httpClient + ?.let { ExternalCoverService.createFactory(it)(context) } + } + put(CoverService::class.java.simpleName, factory) + } } return serviceFactories.values diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt index 0df6f2faaf..55731d639e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt @@ -20,24 +20,18 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationServicesHolder import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.publication.protection.ContentProtection -import org.readium.r2.shared.util.NetworkError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.data.HttpError +import org.readium.r2.shared.util.http.HttpRequest +import org.readium.r2.shared.util.http.HttpResponse +import org.readium.r2.shared.util.http.HttpStreamResponse import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.FailureResource -import org.readium.r2.shared.util.resource.FailureResourceEntry -import org.readium.r2.shared.util.resource.LazyResource -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceEntry -import org.readium.r2.shared.util.resource.StringResource -import org.readium.r2.shared.util.resource.toResourceEntry /** * Provides information about a publication's content protection and manages user rights. */ -public interface ContentProtectionService : Publication.Service { +public interface ContentProtectionService : Publication.WebService { /** * Whether the [Publication] has a restricted access to its resources, and can't be rendered in @@ -74,9 +68,9 @@ public interface ContentProtectionService : Publication.Service { override val links: List get() = RouteHandler.links - override fun get(href: Url): Resource? { - val route = RouteHandler.route(href) ?: return null - return route.handleRequest(href, this) + override suspend fun handle(request: HttpRequest): Try? { + val route = RouteHandler.route(request.url) ?: return null + return route.handleRequest(request, this) } /** @@ -258,7 +252,7 @@ private sealed class RouteHandler { abstract fun acceptRequest(url: Url): Boolean - abstract fun handleRequest(url: Url, service: ContentProtectionService): Resource + abstract suspend fun handleRequest(request: HttpRequest, service: ContentProtectionService): Try object ContentProtectionHandler : RouteHandler() { @@ -273,17 +267,30 @@ private sealed class RouteHandler { override fun acceptRequest(url: Url): Boolean = url.path == path - override fun handleRequest(url: Url, service: ContentProtectionService): Resource = - StringResource(mediaType = mediaType) { - Try.success( - JSONObject().apply { - put("isRestricted", service.isRestricted) - putOpt("error", service.error?.localizedMessage) - putIfNotEmpty("name", service.name) - put("rights", service.rights.toJSON()) - }.toString() - ) + override suspend fun handleRequest(request: HttpRequest, service: ContentProtectionService): Try { + val json = JSONObject().apply { + put("isRestricted", service.isRestricted) + putOpt("error", service.error?.localizedMessage) + putIfNotEmpty("name", service.name) + put("rights", service.rights.toJSON()) } + + val response = HttpResponse( + request = request, + url = request.url, + 200, + emptyMap(), + mediaType + ) + + val body = json + .toString() + .byteInputStream(charset = Charsets.UTF_8) + + return Try.success( + HttpStreamResponse(response, body) + ) + } } object RightsCopyHandler : RouteHandler() { @@ -299,32 +306,23 @@ private sealed class RouteHandler { override fun acceptRequest(url: Url): Boolean = url.path == path - override fun handleRequest(url: Url, service: ContentProtectionService): ResourceEntry = - LazyResource { handleRequestAsync(url, service) }.toResourceEntry(url) - - private suspend fun handleRequestAsync(url: Url, service: ContentProtectionService): ResourceEntry { - val query = url.query + override suspend fun handleRequest(request: HttpRequest, service: ContentProtectionService): Try { + val query = request.url.query val text = query.firstNamedOrNull("text") - ?: return FailureResourceEntry( - url, - ReadError.Network( - NetworkError.BadRequest("'text' parameter is required") - ) + ?: return Try.failure( + badRequestResponse("'text' parameter is required.") ) val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() - ?: return FailureResourceEntry( - url, - ReadError.Network( - NetworkError.BadRequest("If present, 'peek' must be true or false") - ) + ?: return Try.failure( + badRequestResponse("If present, 'peek' must be true or false.") ) val copyAllowed = with(service.rights) { if (peek) canCopy(text) else copy(text) } return if (!copyAllowed) { - FailureResource(ReadError.Network(HttpError(HttpError.Kind.Forbidden))) + Try.failure(forbiddenResponse()) } else { - StringResource("true", MediaType.JSON) + Try.success(trueResponse(request)) } } } @@ -342,28 +340,20 @@ private sealed class RouteHandler { override fun acceptRequest(url: Url): Boolean = url.path == path - override fun handleRequest(url: Url, service: ContentProtectionService): Resource = - LazyResource { handleRequestAsync(url, service) } - private suspend fun handleRequestAsync(url: Url, service: ContentProtectionService): Resource { - val query = url.query + override suspend fun handleRequest(request: HttpRequest, service: ContentProtectionService): Try { + val query = request.url.query val pageCountString = query.firstNamedOrNull("pageCount") - ?: return FailureResource( - ReadError.Network( - NetworkError.BadRequest("'pageCount' parameter is required") - ) + ?: return Try.failure( + badRequestResponse("'pageCount' parameter is required") ) val pageCount = pageCountString.toIntOrNull()?.takeIf { it >= 0 } - ?: return FailureResource( - ReadError.Network( - NetworkError.BadRequest("'pageCount' must be a positive integer") - ) + ?: return Try.failure( + badRequestResponse("'pageCount' must be a positive integer") ) val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() - ?: return FailureResource( - ReadError.Network( - NetworkError.BadRequest("if present, 'peek' must be true or false") - ) + ?: return Try.failure( + badRequestResponse("If present, 'peek' must be true or false") ) val printAllowed = with(service.rights) { @@ -377,9 +367,9 @@ private sealed class RouteHandler { } return if (!printAllowed) { - FailureResource(ReadError.Network(NetworkError.Forbidden())) + Try.failure(forbiddenResponse()) } else { - StringResource("true", mediaType = MediaType.JSON) + Try.success(trueResponse(request)) } } } @@ -395,3 +385,32 @@ private sealed class RouteHandler { put("canPrint", canPrint) } } + +private fun trueResponse(request: HttpRequest): HttpStreamResponse = + HttpStreamResponse( + response = HttpResponse( + request, + request.url, + 200, + emptyMap(), + MediaType.JSON + ), + body = "true".byteInputStream() + ) +private fun forbiddenResponse(): HttpError.Response = + HttpError.Response( + HttpError.Kind.Forbidden, + 403, + null + ) + +private fun badRequestResponse(detail: String): HttpError.Response = + HttpError.Response( + HttpError.Kind.BadRequest, + 400, + null, + JSONObject().apply { + put("title", "Bad request") + put("detail", detail) + }.toString().encodeToByteArray() + ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index 3ad4e604c8..fb59b87507 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -17,14 +17,21 @@ import org.readium.r2.shared.extensions.toPng import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.ServiceFactory -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.publication.firstWithRel +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.data.ReadError +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.HttpError +import org.readium.r2.shared.util.data.readAsBitmap +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpRequest +import org.readium.r2.shared.util.http.HttpResponse +import org.readium.r2.shared.util.http.HttpStreamResponse +import org.readium.r2.shared.util.http.fetch import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.BytesResource -import org.readium.r2.shared.util.resource.FailureResource -import org.readium.r2.shared.util.resource.LazyResource -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceEntry /** * Provides an easy access to a bitmap version of the publication cover. @@ -87,10 +94,67 @@ public var Publication.ServicesBuilder.coverServiceFactory: ServiceFactory? get() = get(CoverService::class) set(value) = set(CoverService::class, value) +internal class ExternalCoverService( + private val coverUrl: AbsoluteUrl, + private val httpClient: HttpClient +) : CoverService { + + override suspend fun cover(): Bitmap? { + val request = HttpRequest(coverUrl) + + val response = httpClient.fetch(request) + .getOrElse { return null } + + return BitmapFactory.decodeByteArray(response.body, 0, response.body.size) + } + + companion object { + + fun createFactory(httpClient: HttpClient): (Publication.Service.Context) -> ExternalCoverService? = { + val manifestUrl = it.manifest + .links + .firstWithRel("self") + ?.url() + + it.manifest + .linksWithRel("cover") + .firstNotNullOfOrNull { link -> link.url(base = manifestUrl) as? AbsoluteUrl } + ?.let { url -> ExternalCoverService(url, httpClient) } + } + } +} + +internal class ResourceCoverService( + private val coverUrl: Url, + private val container: ClosedContainer +) : CoverService { + + override suspend fun cover(): Bitmap? { + val resource = container.get(coverUrl) + ?: return null + + return resource.readAsBitmap() + .getOrNull() + } + + companion object { + + fun createFactory(): (Publication.Service.Context) -> ResourceCoverService? = { + val publicationContent: List = + it.manifest.resources + it.manifest.readingOrder + + publicationContent + .firstWithRel("cover") + ?.url() + ?.let { url -> ResourceCoverService(url, it.container) } + } + } +} + /** * A [CoverService] which provides a unique cover for each Publication. */ -public abstract class GeneratedCoverService : CoverService { +public abstract class GeneratedCoverService : CoverService, Publication.WebService { private val coverLink = Link( href = Url("/~readium/cover")!!, @@ -102,25 +166,30 @@ public abstract class GeneratedCoverService : CoverService { abstract override suspend fun cover(): Bitmap - override fun get(href: Url): Resource? { - if (href != coverLink.url()) { + override suspend fun handle(request: HttpRequest): Try? { + if (request.url != coverLink.url()) { return null } - return LazyResource { - val cover = cover() - val png = cover.toPng() - - if (png == null) { - FailureResource( - ReadError.Content( - MessageError("Unable to convert cover to PNG.") - ) + val cover = cover() + val png = cover.toPng() + ?: return Try.failure( + HttpError.Response( + HttpError.Kind.ServerError, + 500, + null, + null ) - } else { - BytesResource(png, mediaType = MediaType.PNG) - } - } + ) + + val response = HttpResponse(request, request.url, 200, emptyMap(), MediaType.PNG) + + return Try.success( + HttpStreamResponse( + response = response, + body = png.inputStream() + ) + ) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt index 226283d7b8..dba12e839a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt @@ -20,13 +20,17 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationServicesHolder import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.publication.firstWithMediaType +import org.readium.r2.shared.publication.firstWithRel import org.readium.r2.shared.toJSON +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.data.readAsString +import org.readium.r2.shared.util.data.HttpError +import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpRequest +import org.readium.r2.shared.util.http.HttpResponse +import org.readium.r2.shared.util.http.HttpStreamResponse import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.StringResource private val positionsMediaType = MediaType("application/vnd.readium.position-list+json")!! @@ -39,7 +43,7 @@ private val positionsLink = Link( /** * Provides a list of discrete locations in the publication, no matter what the original format is. */ -public interface PositionsService : Publication.Service { +public interface PositionsService : Publication.Service, Publication.WebService { /** * Returns the list of all the positions in the publication, grouped by the resource reading order index. @@ -53,22 +57,25 @@ public interface PositionsService : Publication.Service { override val links: List get() = listOf(positionsLink) - override fun get(href: Url): Resource? { - if (href != positionsLink.url()) { + override suspend fun handle(request: HttpRequest): Try? { + if (request.url != positionsLink.url()) { return null } - return StringResource( - mediaType = positionsMediaType - ) { - val positions = positions() - Try.success( - JSONObject().apply { - put("total", positions.size) - put("positions", positions.toJSON()) - }.toString() - ) + val positions = positions() + + val jsonResponse = JSONObject().apply { + put("total", positions.size) + put("positions", positions.toJSON()) } + + val stream = jsonResponse + .toString() + .byteInputStream(charset = Charsets.UTF_8) + + val response = HttpResponse(request, request.url, 200, emptyMap(), positionsMediaType) + + return Try.success(HttpStreamResponse(response, stream)) } } @@ -146,7 +153,8 @@ public class PerResourcePositionsService( } internal class WebPositionsService( - private val manifest: Manifest + private val manifest: Manifest, + private val httpClient: HttpClient ) : PositionsService { private lateinit var _positions: List @@ -169,20 +177,28 @@ internal class WebPositionsService( return manifest.readingOrder.map { locators[it.url()].orEmpty() } } - private suspend fun computePositions(): List = - links.firstOrNull() - ?.let { get(it.url()) } - ?.readAsString() - ?.getOrNull() + private suspend fun computePositions(): List { + val positionsLink = links.firstOrNull() + ?: return emptyList() + val selfLink = manifest.links.firstWithRel("self") + val positionsUrl = (positionsLink.url(base = selfLink?.url()) as? AbsoluteUrl) + ?: return emptyList() + + return httpClient.stream(HttpRequest(positionsUrl)) + .getOrNull() + ?.body + ?.readBytes() + ?.decodeToString() ?.toJsonOrNull() ?.optJSONArray("positions") ?.mapNotNull { Locator.fromJSON(it as? JSONObject) } .orEmpty() + } companion object { - fun createFactory(): (Publication.Service.Context) -> WebPositionsService = { - WebPositionsService(it.manifest) + fun createFactory(httpClient: HttpClient): (Publication.Service.Context) -> WebPositionsService = { + WebPositionsService(it.manifest, httpClient) } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt index d97f5e0255..eee764c2db 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.data.HttpError @ExperimentalReadiumApi public typealias SearchTry = Try diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt index 49bf51ddbc..33317ef7c8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt @@ -49,7 +49,7 @@ public interface ArchiveFactory { * Creates a new archive [ResourceContainer] to access the entries of the given archive. */ public suspend fun create( - resource: Blob, + resource: Blob, password: String? = null ): Try, Error> } @@ -61,7 +61,7 @@ public class CompositeArchiveFactory( public constructor(vararg factories: ArchiveFactory) : this(factories.toList()) override suspend fun create( - resource: Blob, + resource: Blob, password: String? ): Try, ArchiveFactory.Error> { for (factory in factories) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt index 7d42940781..6531f24f5e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt @@ -13,12 +13,12 @@ import java.util.zip.ZipException import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.readium.r2.shared.util.FilesystemError import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.FilesystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType @@ -44,7 +44,7 @@ public class FileZipArchiveProvider( return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { + override suspend fun sniffBlob(blob: Blob): Try { val file = blob.source?.toFile() ?: return Try.Failure(MediaTypeSnifferError.NotRecognized) @@ -77,7 +77,7 @@ public class FileZipArchiveProvider( } override suspend fun create( - resource: Blob, + resource: Blob, password: String? ): Try, ArchiveFactory.Error> { if (password != null) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt index d93f512f84..a243711f0c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt @@ -15,11 +15,11 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.readFully import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.FilesystemError import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.FilesystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Blob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Blob.kt index 73503d84da..2c3354683c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Blob.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Blob.kt @@ -7,14 +7,13 @@ package org.readium.r2.shared.util.data import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Try /** * Acts as a proxy to an actual data source by handling read access. */ -public interface Blob : SuspendingCloseable { +public interface Blob : SuspendingCloseable { /** * URL locating this resource, if any. @@ -27,7 +26,7 @@ public interface Blob : SuspendingCloseable { * This value must be treated as a hint, as it might not reflect the actual bytes length. To get * the real length, you need to read the whole resource. */ - public suspend fun length(): Try + public suspend fun length(): Try /** * Reads the bytes at the given range. @@ -35,5 +34,5 @@ public interface Blob : SuspendingCloseable { * When [range] is null, the whole content is returned. Out-of-range indexes are clamped to the * available length automatically. */ - public suspend fun read(range: LongRange? = null): Try + public suspend fun read(range: LongRange? = null): Try } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt index 135cfd7ca6..44ff4d7b51 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt @@ -9,7 +9,6 @@ package org.readium.r2.shared.util.data import java.io.IOException import java.io.InputStream import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try /** @@ -18,9 +17,9 @@ import org.readium.r2.shared.util.Try * If you experience bad performances, consider wrapping the stream in a BufferedInputStream. This * is particularly useful when streaming deflated ZIP entries. */ -public class BlobInputStream( - private val blob: Blob, - private val wrapError: (E) -> IOException, +public class BlobInputStream( + private val blob: Blob, + private val wrapError: (ReadError) -> IOException, private val range: LongRange? = null ) : InputStream() { @@ -46,9 +45,9 @@ public class BlobInputStream( */ private var mark: Long = range?.start ?: 0 - private var error: E? = null + private var error: ReadError? = null - internal fun consumeError(): E? { + internal fun consumeError(): ReadError? { val errorNow = error error = null return errorNow @@ -141,7 +140,7 @@ public class BlobInputStream( } } - private fun Try.recover(): S = + private fun Try.recover(): S = when (this) { is Try.Success -> { value diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index 7c4389c83e..00739f0bc7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.util.resource.Resource /** * Represents a container entry's. */ -public interface ContainerEntry : Blob { +public interface ContainerEntry : Blob { /** * URL used to access the resource in the container. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt index 9d545aa079..be1000cd8d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt @@ -16,7 +16,6 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.FilesystemError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.toUrl @@ -26,7 +25,7 @@ import org.readium.r2.shared.util.toUrl public class ContentBlob( private val uri: Uri, private val contentResolver: ContentResolver -) : Blob { +) : Blob { private lateinit var _length: Try diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index 613a767c73..93d3956c83 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -23,23 +23,23 @@ import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.xml.ElementNode import org.readium.r2.shared.util.xml.XmlParser -public sealed class DecoderError( +public sealed class DecoderError( override val message: String ) : Error { - public class DataAccess( - override val cause: E - ) : DecoderError("Data source error") + public class DataAccess( + override val cause: ReadError + ) : DecoderError("Data source error") - public class DecodingError( + public class DecodingError( override val cause: Error? - ) : DecoderError("Decoding Error") + ) : DecoderError("Decoding Error") } -internal suspend fun Try.decode( +internal suspend fun Try.decode( block: (value: S) -> R, wrapException: (Exception) -> Error -): Try> = +): Try = when (this) { is Try.Success -> try { @@ -55,10 +55,10 @@ internal suspend fun Try.decode( Try.failure(DecoderError.DataAccess(value)) } -internal suspend fun Try>.decodeMap( +internal suspend fun Try.decodeMap( block: (value: S) -> R, wrapException: (Exception) -> Error -): Try> = +): Try = when (this) { is Try.Success -> try { @@ -80,16 +80,16 @@ internal suspend fun Try>.decodeMap( * It will extract the charset parameter from the media type hints to figure out an encoding. * Otherwise, fallback on UTF-8. */ -public suspend fun Blob.readAsString( +public suspend fun Blob.readAsString( charset: Charset = Charsets.UTF_8 -): Try> = +): Try = read().decode( { String(it, charset = charset) }, { MessageError("Content is not a valid $charset string.", ThrowableError(it)) } ) /** Content as an XML document. */ -public suspend fun Blob.readAsXml(): Try> = +public suspend fun Blob.readAsXml(): Try = read().decode( { XmlParser().parse(ByteArrayInputStream(it)) }, { MessageError("Content is not a valid XML document.", ThrowableError(it)) } @@ -98,14 +98,14 @@ public suspend fun Blob.readAsXml(): Try Blob.readAsJson(): Try> = +public suspend fun Blob.readAsJson(): Try = readAsString().decodeMap( { JSONObject(it) }, { MessageError("Content is not valid JSON.", ThrowableError(it)) } ) /** Readium Web Publication Manifest parsed from the content. */ -public suspend fun Blob.readAsRwpm(): Try> = +public suspend fun Blob.readAsRwpm(): Try = readAsJson().flatMap { json -> Manifest.fromJSON(json) ?.let { Try.success(it) } @@ -119,7 +119,7 @@ public suspend fun Blob.readAsRwpm(): Try Blob.readAsBitmap(): Try> = +public suspend fun Blob.readAsBitmap(): Try = read() .mapFailure { DecoderError.DataAccess(it) } .flatMap { bytes -> @@ -135,9 +135,9 @@ public suspend fun Blob.readAsBitmap(): Try Blob.containsJsonKeys( +public suspend fun Blob.containsJsonKeys( vararg keys: String -): Try> { +): Try { val json = readAsJson() .getOrElse { return Try.failure(it) } return Try.success(json.keys().asSequence().toSet().containsAll(keys.toList())) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt index 47a6a12565..aa56e458cb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt @@ -15,7 +15,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.FilesystemError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.isLazyInitialized @@ -26,7 +25,7 @@ import org.readium.r2.shared.util.toUrl */ public class FileBlob( private val file: File -) : Blob { +) : Blob { private val randomAccessFile by lazy { try { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/FilesystemError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FilesystemError.kt similarity index 87% rename from readium/shared/src/main/java/org/readium/r2/shared/util/FilesystemError.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/FilesystemError.kt index 7bd3cbf782..b7ed6bcaa0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/FilesystemError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FilesystemError.kt @@ -4,7 +4,10 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util +package org.readium.r2.shared.util.data + +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ThrowableError public sealed class FilesystemError( override val message: String, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpError.kt new file mode 100644 index 0000000000..a4f0824049 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpError.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2021 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import org.json.JSONObject +import org.readium.r2.shared.extensions.tryOrLog +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.http.ProblemDetails +import org.readium.r2.shared.util.mediatype.MediaType + +/** + * Represents an error occurring during an HTTP activity. + */ +public sealed class HttpError( + public override val message: String, + public override val cause: Error? = null +) : Error { + + public class MalformedResponse(cause: Error?) : + HttpError("The received response could not be decoded.", cause) + + /** The client, server or gateways timed out. */ + public class Timeout(cause: Error) : + HttpError("Request timed out.", cause) + + public class UnreachableHost(cause: Error) : + HttpError("Host could not be reached.", cause) + + public class Cancelled(cause: Error) : + HttpError("The request was cancelled.", cause) + + /** An unknown networking error. */ + public class Other(cause: Error) : + HttpError("A networking error occurred.", cause) + + /* + * @param kind Category of HTTP error. + * @param mediaType Response media type. + * @param body Response body. + */ + public class Response( + public val kind: Kind, + public val statusCode: Int, + public val mediaType: MediaType? = null, + public val body: ByteArray? = null + ) : HttpError(kind.message, null) { + + /** Response body parsed as a JSON problem details. */ + public val problemDetails: ProblemDetails? by lazy { + if (body == null || mediaType?.matches(MediaType.JSON_PROBLEM_DETAILS) != true) { + return@lazy null + } + + tryOrLog { ProblemDetails.fromJSON(JSONObject(String(body))) } + } + + public companion object { + + /** + * Creates an HTTP error from a status code. + * + * Returns null if the status code is a success. + */ + public operator fun invoke( + statusCode: Int, + mediaType: MediaType? = null, + body: ByteArray? = null + ): HttpError? = + Kind.ofStatusCode(statusCode)?.let { kind -> + Response(kind, statusCode, mediaType, body) + } + } + } + + public enum class Kind(public val message: String) { + /** (400) The server cannot or will not process the request due to an apparent client error. */ + BadRequest("The provided request was not valid."), + + /** (401) Authentication is required and has failed or has not yet been provided. */ + Unauthorized("Authentication required."), + + /** (403) The server refuses the action, probably because we don't have the necessary permissions. */ + Forbidden("You are not authorized."), + + /** (404) The requested resource could not be found. */ + NotFound("Page not found."), + + /** (405) Method not allowed. */ + MethodNotAllowed("Method not allowed."), + + /** (4xx) Other client errors */ + ClientError("A client error occurred."), + + /** (5xx) Server errors */ + ServerError("A server error occurred, please try again later."); + + public companion object { + + /** Resolves the kind of the HTTP error associated with the given [statusCode]. */ + public fun ofStatusCode(statusCode: Int): Kind? = + when (statusCode) { + in 200..399 -> null + 400 -> BadRequest + 401 -> Unauthorized + 403 -> Forbidden + 404 -> NotFound + 405 -> MethodNotAllowed + in 406..499 -> ClientError + in 500..599 -> ServerError + else -> null + } + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/InMemoryBlob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/InMemoryBlob.kt new file mode 100644 index 0000000000..1e79db7f32 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/InMemoryBlob.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import kotlinx.coroutines.runBlocking +import org.readium.r2.shared.extensions.coerceFirstNonNegative +import org.readium.r2.shared.extensions.read +import org.readium.r2.shared.extensions.requireLengthFitInt +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try + +/** Creates a Blob serving a [ByteArray]. */ +public class InMemoryBlob( + override val source: AbsoluteUrl?, + private val bytes: suspend () -> Try +) : Blob { + + public constructor( + bytes: ByteArray, + source: AbsoluteUrl? = null + ) : this(source = source, { Try.success(bytes) }) + + override suspend fun length(): Try = + read().map { it.size.toLong() } + + private lateinit var _bytes: Try + + override suspend fun read(range: LongRange?): Try { + if (!::_bytes.isInitialized) { + _bytes = bytes() + } + + if (range == null) { + return _bytes + } + + @Suppress("NAME_SHADOWING") + val range = range + .coerceFirstNonNegative() + .requireLengthFitInt() + + if (range.isEmpty()) { + return Try.success(ByteArray(0)) + } + + return _bytes.map { it.read(range) } + } + + override suspend fun close() {} + + override fun toString(): String = + "${javaClass.simpleName}(${runBlocking { length() }} bytes)" +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt index 2e73f53262..0ce09c1b1d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt @@ -7,9 +7,9 @@ package org.readium.r2.shared.util.data import java.io.IOException +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ErrorException -import org.readium.r2.shared.util.FilesystemError import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.ThrowableError @@ -21,7 +21,7 @@ public sealed class ReadError( override val cause: Error? = null ) : Error { - public class Network(public override val cause: Error) : + public class Network(public override val cause: HttpError) : ReadError("A network error occurred.", cause) public class Filesystem(public override val cause: FilesystemError) : @@ -54,18 +54,19 @@ public sealed class ReadError( } } -public class AccessException( +public class ReadException( public val error: ReadError ) : IOException(error.message, ErrorException(error)) -internal fun Exception.unwrapAccessException(): Exception { - fun Throwable.findResourceExceptionCause(): AccessException? = +@InternalReadiumApi +public fun Exception.unwrapReadException(): Exception { + fun Throwable.findReadExceptionCause(): ReadException? = when { - this is AccessException -> this - cause != null -> cause!!.findResourceExceptionCause() + this is ReadException -> this + cause != null -> cause!!.findReadExceptionCause() else -> null } - this.findResourceExceptionCause()?.let { return it } + this.findReadExceptionCause()?.let { return it } return this } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt index 0444588d40..c220f6e132 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt @@ -7,7 +7,7 @@ package org.readium.r2.shared.util.downloads import java.io.File -import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager import org.readium.r2.shared.util.downloads.foreground.ForegroundDownloadManager import org.readium.r2.shared.util.mediatype.MediaType @@ -24,7 +24,7 @@ import org.readium.r2.shared.util.mediatype.MediaType public interface DownloadManager { public data class Request( - val url: Url, + val url: AbsoluteUrl, val headers: Map> = emptyMap() ) @@ -42,7 +42,7 @@ public interface DownloadManager { ) : org.readium.r2.shared.util.Error { public class HttpError( - cause: org.readium.r2.shared.util.http.HttpError + cause: org.readium.r2.shared.util.data.HttpError ) : Error(cause.message, cause) public class DeviceNotFound( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index 34ef13697a..320625e224 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -27,9 +27,9 @@ import org.readium.r2.shared.extensions.tryOr import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.FileBlob +import org.readium.r2.shared.util.data.HttpError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints @@ -311,9 +311,11 @@ public class AndroidDownloadManager internal constructor( SystemDownloadManager.ERROR_UNHANDLED_HTTP_CODE -> DownloadManager.Error.HttpError(httpErrorForCode(code)) SystemDownloadManager.ERROR_HTTP_DATA_ERROR -> - DownloadManager.Error.HttpError(HttpError(HttpError.Kind.Other)) + DownloadManager.Error.HttpError(HttpError.MalformedResponse(null)) SystemDownloadManager.ERROR_TOO_MANY_REDIRECTS -> - DownloadManager.Error.HttpError(HttpError(HttpError.Kind.TooManyRedirects)) + DownloadManager.Error.HttpError( + HttpError.Cancelled(MessageError("Too many redirects.")) + ) SystemDownloadManager.ERROR_CANNOT_RESUME -> DownloadManager.Error.CannotResume() SystemDownloadManager.ERROR_DEVICE_NOT_FOUND -> @@ -329,7 +331,8 @@ public class AndroidDownloadManager internal constructor( } private fun httpErrorForCode(code: Int): HttpError = - HttpError(code) ?: HttpError(HttpError.Kind.Other) + HttpError.Response(code) + ?: HttpError.MalformedResponse(MessageError("Unknown HTTP status code.")) public override fun close() { listeners.clear() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt index f52b36b0af..c1408cc52e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt @@ -19,10 +19,10 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.HttpError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpResponse import org.readium.r2.shared.util.http.HttpTry @@ -64,7 +64,7 @@ public class ForegroundDownloadManager( httpClient .download( request = HttpRequest( - url = request.url.toString(), + url = request.url, headers = request.headers ), destination = destination, @@ -148,6 +148,6 @@ public class ForegroundDownloadManager( } } } catch (e: Exception) { - Try.failure(HttpError(HttpError.Kind.Other, cause = ThrowableError(e))) + Try.failure(HttpError.Other(ThrowableError(e))) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index 97b634db0b..b160a2c167 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -11,7 +11,6 @@ import java.io.ByteArrayInputStream import java.io.FileInputStream import java.io.InputStream import java.net.HttpURLConnection -import java.net.MalformedURLException import java.net.SocketTimeoutException import java.net.URL import java.util.concurrent.CancellationException @@ -20,8 +19,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.joinValues import org.readium.r2.shared.extensions.lowerCaseKeys +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.MessageError 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.data.HttpError +import org.readium.r2.shared.util.data.InMemoryBlob import org.readium.r2.shared.util.e import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrDefault @@ -29,7 +33,6 @@ import org.readium.r2.shared.util.http.HttpRequest.Method 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 org.readium.r2.shared.util.resource.BytesResource import org.readium.r2.shared.util.tryRecover import timber.log.Timber @@ -115,14 +118,17 @@ public class DefaultHttpClient( * You can return either: * - the provided [newRequest] to proceed with the redirection * - a different redirection request - * - a [HttpError.CANCELLED] error to abort the redirection */ public suspend fun onFollowUnsafeRedirect( request: HttpRequest, response: HttpResponse, newRequest: HttpRequest ): HttpTry = - Try.failure(HttpError.CANCELLED) + Try.failure( + HttpError.Cancelled( + MessageError("Request cancelled because of an unsafe redirect.") + ) + ) /** * Called when the HTTP client received an HTTP response for the given [request]. @@ -172,17 +178,19 @@ public class DefaultHttpClient( val mediaType = body?.let { mediaTypeRetriever.retrieve( hints = MediaTypeHints(connection), - blob = BytesResource { it } + blob = InMemoryBlob(it) ).getOrDefault(MediaType.BINARY) } - return@withContext Try.failure(HttpError(kind, mediaType, body)) + return@withContext Try.failure( + HttpError.Response(kind, statusCode, mediaType, body) + ) } val mediaType = mediaTypeRetriever.retrieve(MediaTypeHints(connection)) val response = HttpResponse( request = request, - url = connection.url.toString(), + url = request.url, statusCode = statusCode, headers = connection.safeHeaders, mediaType = mediaType ?: MediaType.BINARY @@ -208,7 +216,7 @@ public class DefaultHttpClient( return callback.onStartRequest(request) .flatMap { tryStream(it) } .tryRecover { error -> - if (error.kind != HttpError.Kind.Cancelled) { + if (error !is HttpError.Cancelled) { callback.onRecoverRequest(request, error) .flatMap { stream(it) } } else { @@ -236,11 +244,20 @@ public class DefaultHttpClient( // > https://www.rfc-editor.org/rfc/rfc1945.html#section-9.3 val redirectCount = request.extras.getInt(EXTRA_REDIRECT_COUNT) if (redirectCount > 5) { - return Try.failure(HttpError(HttpError.Kind.TooManyRedirects)) + return Try.failure( + HttpError.Cancelled( + MessageError("There were too many redirects to follow.") + ) + ) } val location = response.header("Location") - ?: return Try.failure(HttpError(kind = HttpError.Kind.MalformedResponse)) + ?.let { Url(it)?.resolve(request.url) as? AbsoluteUrl } + ?: return Try.failure( + HttpError.MalformedResponse( + MessageError("Location of redirect is missing or invalid.") + ) + ) val newRequest = HttpRequest( url = location, @@ -262,7 +279,7 @@ public class DefaultHttpClient( } private fun HttpRequest.toHttpURLConnection(): HttpURLConnection { - val url = URL(url) + val url = URL(url.toString()) val connection = (url.openConnection() as HttpURLConnection) connection.requestMethod = method.name @@ -318,17 +335,16 @@ public class DefaultHttpClient( /** * Creates an HTTP error from a generic exception. */ -private fun wrap(cause: Throwable): HttpError { - val kind = when (cause) { - is MalformedURLException -> HttpError.Kind.MalformedRequest - is CancellationException -> HttpError.Kind.Cancelled - is SocketTimeoutException -> HttpError.Kind.Timeout - else -> HttpError.Kind.Other +private fun wrap(cause: Throwable): HttpError = + when (cause) { + is CancellationException -> + throw cause + is SocketTimeoutException -> + HttpError.Timeout(ThrowableError(cause)) + else -> + HttpError.Other(ThrowableError(cause)) } - return HttpError(kind = kind, cause = ThrowableError(cause)) -} - /** * [HttpURLConnection]'s input stream which disconnects when closed. */ diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt index 77da2aaed6..55a765da39 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt @@ -12,12 +12,16 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject import org.readium.r2.shared.ExperimentalReadiumApi +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.data.HttpError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.tryRecover +public typealias HttpTry = Try + /** * An HTTP client performs HTTP requests. * @@ -48,7 +52,7 @@ public interface HttpClient { */ public data class HttpResponse( val request: HttpRequest, - val url: String, + val url: AbsoluteUrl, val statusCode: Int, val headers: Map>, val mediaType: MediaType @@ -131,7 +135,7 @@ public suspend fun HttpClient.fetch(request: HttpRequest): HttpTry HttpClient.fetchWithDecoder( ) } catch (e: Exception) { Try.failure( - HttpError(kind = HttpError.Kind.MalformedResponse, cause = ThrowableError(e)) + HttpError.MalformedResponse(ThrowableError(e)) ) } } @@ -200,9 +204,9 @@ public suspend fun HttpClient.head(request: HttpRequest): HttpTry return request .copy { method = HttpRequest.Method.HEAD } .response() - .tryRecover { exception -> - if (exception.kind != HttpError.Kind.MethodNotAllowed) { - return@tryRecover Try.failure(exception) + .tryRecover { error -> + if (error !is HttpError.Response || error.kind != HttpError.Kind.MethodNotAllowed) { + return@tryRecover Try.failure(error) } request diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt deleted file mode 100644 index 2c89783d21..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2021 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.http - -import org.json.JSONObject -import org.readium.r2.shared.extensions.tryOrLog -import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.mediatype.MediaType - -public typealias HttpTry = Try - -/** - * Represents an error occurring during an HTTP activity. - * - * @param kind Category of HTTP error. - * @param mediaType Response media type. - * @param body Response body. - * @param cause Underlying error, if any. - */ -public class HttpError( - public val kind: Kind, - public val mediaType: MediaType? = null, - public val body: ByteArray? = null, - public override val cause: Error? = null -) : Error { - - public enum class Kind(public val message: String) { - /** The provided request was not valid. */ - MalformedRequest("The provided request was not valid."), - - /** The received response couldn't be decoded. */ - MalformedResponse("The received response could not be decoded."), - - /** The client, server or gateways timed out. */ - Timeout("Request timed out."), - - /** (400) The server cannot or will not process the request due to an apparent client error. */ - BadRequest("The provided request was not valid."), - - /** (401) Authentication is required and has failed or has not yet been provided. */ - Unauthorized("Authentication required."), - - /** (403) The server refuses the action, probably because we don't have the necessary permissions. */ - Forbidden("You are not authorized."), - - /** (404) The requested resource could not be found. */ - NotFound("Page not found."), - - /** (405) Method not allowed. */ - MethodNotAllowed("Method not allowed."), - - /** (4xx) Other client errors */ - ClientError("A client error occurred."), - - /** (5xx) Server errors */ - ServerError("A server error occurred, please try again later."), - - /** The device is offline. */ - Offline("Your Internet connection appears to be offline."), - - /** Too many redirects */ - TooManyRedirects("There were too many redirects to follow."), - - /** The request was cancelled. */ - Cancelled("The request was cancelled."), - - /** An error whose kind is not recognized. */ - Other("A networking error occurred."); - - public companion object { - - /** Resolves the kind of the HTTP error associated with the given [statusCode]. */ - public fun ofStatusCode(statusCode: Int): Kind? = - when (statusCode) { - in 200..399 -> null - 400 -> BadRequest - 401 -> Unauthorized - 403 -> Forbidden - 404 -> NotFound - 405 -> MethodNotAllowed - in 406..498 -> ClientError - 499 -> Cancelled - in 500..599 -> ServerError - else -> MalformedResponse - } - } - } - - override val message: String - get() = kind.message - - /** Response body parsed as a JSON problem details. */ - public val problemDetails: ProblemDetails? by lazy { - if (body == null || mediaType?.matches(MediaType.JSON_PROBLEM_DETAILS) != true) { - return@lazy null - } - - tryOrLog { ProblemDetails.fromJSON(JSONObject(String(body))) } - } - - public companion object { - - /** - * Shortcut for a cancelled HTTP error. - */ - public val CANCELLED: HttpError = HttpError(kind = Kind.Cancelled) - - /** - * Creates an HTTP error from a status code. - * - * Returns null if the status code is a success. - */ - public operator fun invoke( - statusCode: Int, - mediaType: MediaType? = null, - body: ByteArray? = null - ): HttpError? = - Kind.ofStatusCode(statusCode)?.let { kind -> - HttpError(kind, mediaType, body) - } - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt index 9ad23c1418..499874240b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt @@ -12,6 +12,8 @@ import java.io.Serializable import java.net.URLEncoder import kotlin.time.Duration import org.readium.r2.shared.extensions.toMutable +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.toUri /** * Holds the information about an HTTP request performed by an [HttpClient]. @@ -30,7 +32,7 @@ import org.readium.r2.shared.extensions.toMutable * as popping up an authentication dialog. */ public class HttpRequest( - public val url: String, + public val url: AbsoluteUrl, public val method: Method = Method.GET, public val headers: Map> = mapOf(), public val body: Body? = null, @@ -66,12 +68,12 @@ public class HttpRequest( buildUpon().apply(build).build() public companion object { - public operator fun invoke(url: String, build: Builder.() -> Unit): HttpRequest = + public operator fun invoke(url: AbsoluteUrl, build: Builder.() -> Unit): HttpRequest = Builder(url).apply(build).build() } public class Builder( - url: String, + public var url: AbsoluteUrl, public var method: Method = Method.GET, public var headers: MutableMap> = mutableMapOf(), public var body: Body? = null, @@ -81,11 +83,7 @@ public class HttpRequest( public var allowUserInteraction: Boolean = false ) { - public var url: String - get() = uriBuilder.build().toString() - set(value) { uriBuilder = Uri.parse(value).buildUpon() } - - private var uriBuilder: Uri.Builder = Uri.parse(url).buildUpon() + private var uriBuilder: Uri.Builder = url.toUri().buildUpon() public fun appendQueryParameter(key: String, value: String?): Builder { if (value != null) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index 64579723a2..5a43ef1429 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -8,8 +8,8 @@ import org.readium.r2.shared.extensions.read import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError -import org.readium.r2.shared.util.NetworkError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.HttpError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.io.CountingInputStream @@ -66,8 +66,8 @@ public class HttpResource( return _headResponse } - _headResponse = client.head(HttpRequest(source.toString())) - .mapFailure { it.wrap() } + _headResponse = client.head(HttpRequest(source)) + .mapFailure { ReadError.Network(it) } return _headResponse } @@ -91,7 +91,7 @@ public class HttpResource( } tryOrLog { inputStream?.close() } - val request = HttpRequest(source.toString()) { + val request = HttpRequest(source) { from?.let { setRange(from..-1) } } @@ -100,13 +100,13 @@ public class HttpResource( if (from != null && response.response.statusCode != 206 ) { val error = MessageError("Server seems not to support range requests.") - Try.failure(HttpError(HttpError.Kind.Other, cause = error)) + Try.failure(HttpError.Other(error)) } else { Try.success(response) } } .map { CountingInputStream(it.body) } - .mapFailure { it.wrap() } + .mapFailure { ReadError.Network(it) } .onSuccess { inputStream = it inputStreamStart = from ?: 0 @@ -116,22 +116,6 @@ public class HttpResource( private var inputStream: CountingInputStream? = null private var inputStreamStart = 0L - private fun HttpError.wrap(): ReadError = - when (this.kind) { - HttpError.Kind.MalformedRequest, HttpError.Kind.BadRequest, HttpError.Kind.MethodNotAllowed -> - ReadError.Network(NetworkError.BadRequest(cause = this)) - HttpError.Kind.Timeout, HttpError.Kind.Offline -> - ReadError.Network(NetworkError.Offline(this)) - HttpError.Kind.Unauthorized, HttpError.Kind.Forbidden -> - ReadError.Network(NetworkError.Forbidden(this)) - HttpError.Kind.NotFound -> - ReadError.Network(NetworkError.NotFound(this)) - HttpError.Kind.Cancelled, HttpError.Kind.TooManyRedirects -> - ReadError.Other(this) - HttpError.Kind.MalformedResponse, HttpError.Kind.ClientError, HttpError.Kind.ServerError, HttpError.Kind.Other -> - ReadError.Other(this) - } - public companion object { private const val MAX_SKIP_BYTES: Long = 8192 diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt index 5b3f689b31..e55834f356 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt @@ -9,7 +9,6 @@ package org.readium.r2.shared.util.mediatype import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.ReadError /** * The default composite sniffer provided by Readium for all known formats. @@ -39,7 +38,7 @@ public class DefaultMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try = sniffer.sniffHints(hints) - override suspend fun sniffBlob(blob: Blob): Try = + override suspend fun sniffBlob(blob: Blob): Try = sniffer.sniffBlob(blob) override suspend fun sniffContainer(container: Container<*>): Try = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt index a8ce3b5943..22c0de6d39 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt @@ -14,7 +14,6 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.FileBlob -import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.toUri /** @@ -113,7 +112,7 @@ public class MediaTypeRetriever( */ public suspend fun retrieve( hints: MediaTypeHints = MediaTypeHints(), - blob: Blob? = null + blob: Blob? = null ): Try { mediaTypeSniffer.sniffHints(hints) .getOrNull() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index e327852cd2..c5cf03e422 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -53,7 +53,7 @@ public interface HintMediaTypeSniffer { public interface BlobMediaTypeSniffer { public suspend fun sniffBlob( - blob: Blob + blob: Blob ): Try } @@ -83,7 +83,7 @@ public interface MediaTypeSniffer : * Sniffs a [MediaType] from a [Blob]. */ public override suspend fun sniffBlob( - blob: Blob + blob: Blob ): Try = Try.failure(MediaTypeSnifferError.NotRecognized) @@ -110,7 +110,7 @@ internal open class CompositeMediaTypeSniffer( return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { + override suspend fun sniffBlob(blob: Blob): Try { for (sniffer in sniffers) { sniffer.sniffBlob(blob) .getOrElse { error -> @@ -162,7 +162,7 @@ public class XhtmlMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { + override suspend fun sniffBlob(blob: Blob): Try { blob.readAsXml() .getOrElse { when (it) { @@ -198,7 +198,7 @@ public class HtmlMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { + override suspend fun sniffBlob(blob: Blob): Try { // [contentAsXml] will fail if the HTML is not a proper XML document, hence the doctype check. blob.readAsXml() .getOrElse { @@ -266,7 +266,7 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { + override suspend fun sniffBlob(blob: Blob): Try { // OPDS 1 blob.readAsXml() .getOrElse { @@ -349,7 +349,7 @@ public object LcpLicenseMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { + override suspend fun sniffBlob(blob: Blob): Try { blob.containsJsonKeys("id", "issued", "provider", "encryption") .getOrElse { when (it) { @@ -440,7 +440,7 @@ public class WebPubManifestMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - public override suspend fun sniffBlob(blob: Blob): Try { + public override suspend fun sniffBlob(blob: Blob): Try { val manifest: Manifest = blob.readAsRwpm() .getOrElse { @@ -548,7 +548,7 @@ public class WebPubMediaTypeSniffer : MediaTypeSniffer { /** Sniffs a W3C Web Publication Manifest. */ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { - override suspend fun sniffBlob(blob: Blob): Try { + override suspend fun sniffBlob(blob: Blob): Try { // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. val string = blob.readAsString() .getOrElse { @@ -762,7 +762,7 @@ public object PdfMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { + override suspend fun sniffBlob(blob: Blob): Try { blob.read(0L until 5L) .getOrElse { error -> return Try.failure(MediaTypeSnifferError.DataAccess(error)) @@ -785,7 +785,7 @@ public object JsonMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { + override suspend fun sniffBlob(blob: Blob): Try { blob.readAsJson() .getOrElse { when (it) { @@ -824,7 +824,7 @@ public class SystemMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { + override suspend fun sniffBlob(blob: Blob): Try { BlobInputStream(blob, ::SystemSnifferException) .use { stream -> try { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt index e02fc8901f..d04e373f37 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt @@ -10,9 +10,9 @@ import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.tryRecover internal class KnownMediaTypeResourceAdapter( - private val blob: Blob, + private val blob: Blob, private val mediaType: MediaType -) : Resource, Blob by blob { +) : Resource, Blob by blob { override suspend fun mediaType(): Try = Try.success(mediaType) @@ -23,10 +23,10 @@ internal class KnownMediaTypeResourceAdapter( } internal class GuessMediaTypeResourceAdapter( - private val blob: Blob, + private val blob: Blob, private val mediaTypeRetriever: MediaTypeRetriever, private val mediaTypeHints: MediaTypeHints -) : Resource, Blob by blob { +) : Resource, Blob by blob { override suspend fun mediaType(): Try = mediaTypeRetriever.retrieve( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt deleted file mode 100644 index ca29dbe0c7..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BytesResource.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.resource - -import kotlinx.coroutines.runBlocking -import org.readium.r2.shared.extensions.coerceFirstNonNegative -import org.readium.r2.shared.extensions.read -import org.readium.r2.shared.extensions.requireLengthFitInt -import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.assertSuccess -import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.mediatype.MediaType - -public sealed class BaseBytesResource( - override val source: AbsoluteUrl?, - private val mediaType: MediaType, - private val properties: Resource.Properties, - protected val bytes: suspend () -> Try -) : Resource { - - override suspend fun properties(): Try = - Try.success(properties) - - override suspend fun mediaType(): Try = - Try.success(mediaType) - - override suspend fun length(): Try = - read().map { it.size.toLong() } - - private lateinit var _bytes: Try - - override suspend fun read(range: LongRange?): Try { - if (!::_bytes.isInitialized) { - _bytes = bytes() - } - - if (range == null) { - return _bytes - } - - @Suppress("NAME_SHADOWING") - val range = range - .coerceFirstNonNegative() - .requireLengthFitInt() - - if (range.isEmpty()) { - return Try.success(ByteArray(0)) - } - - return _bytes.map { it.read(range) } - } - - override suspend fun close() {} -} - -/** Creates a Resource serving a [ByteArray]. */ -public class BytesResource( - source: AbsoluteUrl? = null, - mediaType: MediaType, - properties: Resource.Properties = Resource.Properties(), - bytes: suspend () -> Try -) : BaseBytesResource( - source = source, - mediaType = mediaType, - properties = properties, - bytes = bytes -) { - - public constructor( - bytes: ByteArray, - mediaType: MediaType, - url: AbsoluteUrl? = null, - properties: Resource.Properties = Resource.Properties() - ) : - this(source = url, mediaType = mediaType, properties = properties, { Try.success(bytes) }) - - override fun toString(): String = - "${javaClass.simpleName}(${runBlocking { length() }} bytes)" -} - -/** Creates a Resource serving a [String]. */ -public class StringResource( - source: AbsoluteUrl? = null, - mediaType: MediaType, - properties: Resource.Properties = Resource.Properties(), - string: suspend () -> Try -) : BaseBytesResource( - source = source, - mediaType = mediaType, - properties = properties, - { string().map { it.toByteArray() } } -) { - - public constructor( - string: String, - mediaType: MediaType, - url: AbsoluteUrl? = null, - properties: Resource.Properties = Resource.Properties() - ) : - this(source = url, mediaType = mediaType, properties = properties, { Try.success(string) }) - - override fun toString(): String = - "${javaClass.simpleName}(${runBlocking { read().assertSuccess().decodeToString() } }})" -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index 5f4d902f87..f3b841ceb6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.util.mediatype.MediaType /** * Acts as a proxy to an actual resource by handling read access. */ -public interface Resource : Blob { +public interface Resource : Blob { /** * Returns the resource media type if known. @@ -68,19 +68,12 @@ public class FailureResource( replaceWith = ReplaceWith("map(transform)") ) @Suppress("UnusedReceiverParameter") -public fun Try.mapCatching(): ResourceTry = +public fun Try.mapCatching(): ResourceTry = throw NotImplementedError() -public inline fun Try.flatMapCatching( - transform: (value: S) -> ResourceTry -): ResourceTry = - flatMap { - try { - transform(it) - } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. - Try.failure(ContainerEntry(ReadError.OutOfMemory(e))) - } - } +@Suppress("UnusedReceiverParameter") +public fun Try.flatMapCatching(): ResourceTry = + throw NotImplementedError() internal fun Resource.withMediaType(mediaType: MediaType?): Resource { if (mediaType == null) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt new file mode 100644 index 0000000000..2a5af9f953 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import kotlinx.coroutines.runBlocking +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.InMemoryBlob +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.mediatype.MediaType + +/** Creates a Resource serving a [String]. */ +public class StringResource( + private val blob: Blob, + private val mediaType: MediaType, + private val properties: Resource.Properties +) : Resource, Blob by blob { + + public constructor( + mediaType: MediaType, + source: AbsoluteUrl? = null, + properties: Resource.Properties = Resource.Properties(), + string: suspend () -> Try + ) : this(InMemoryBlob(source) { string().map { it.toByteArray() } }, mediaType, properties) + + public constructor( + string: String, + mediaType: MediaType, + source: AbsoluteUrl? = null, + properties: Resource.Properties = Resource.Properties() + ) : this(mediaType, source, properties, { Try.success(string) }) + + override suspend fun mediaType(): Try = + Try.success(mediaType) + + override suspend fun properties(): Try = + Try.success(properties) + + override fun toString(): String = + "${javaClass.simpleName}(${runBlocking { read().assertSuccess().decodeToString() } }})" +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/DatasourceChannel.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/BlobChannel.kt similarity index 94% rename from readium/shared/src/main/java/org/readium/r2/shared/util/zip/DatasourceChannel.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/zip/BlobChannel.kt index 6701311320..ab3e755816 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/DatasourceChannel.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/BlobChannel.kt @@ -14,16 +14,16 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.zip.jvm.ClosedChannelException import org.readium.r2.shared.util.zip.jvm.NonWritableChannelException import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel -internal class DatasourceChannel( - private val blob: Blob, - private val wrapError: (E) -> IOException +internal class BlobChannel( + private val blob: Blob, + private val wrapError: (ReadError) -> IOException ) : SeekableByteChannel { private val coroutineScope: CoroutineScope = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index bf7ea7e6b6..9291d804b2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -16,10 +16,10 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.archive.ArchiveProperties import org.readium.r2.shared.util.archive.archive -import org.readium.r2.shared.util.data.AccessException import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.unwrapAccessException +import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.data.unwrapReadException import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream import org.readium.r2.shared.util.mediatype.MediaType @@ -94,8 +94,8 @@ internal class ChannelZipContainer( } Try.success(bytes) } catch (exception: Exception) { - when (val e = exception.unwrapAccessException()) { - is AccessException -> + when (val e = exception.unwrapReadException()) { + is ReadException -> Try.failure(e.error) else -> Try.failure(ReadError.Content(e)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 9c8c69c5ac..0170c1ba08 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -14,11 +14,11 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.archive.ArchiveProvider -import org.readium.r2.shared.util.data.AccessException import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.unwrapAccessException +import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.data.unwrapReadException import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeRetriever @@ -47,13 +47,13 @@ public class StreamingZipArchiveProvider( return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { + override suspend fun sniffBlob(blob: Blob): Try { return try { - openDataSource(blob, ::AccessException, null) + openBlob(blob, ::ReadException, null) Try.success(MediaType.ZIP) } catch (exception: Exception) { - when (val e = exception.unwrapAccessException()) { - is AccessException -> + when (val e = exception.unwrapReadException()) { + is ReadException -> Try.failure(MediaTypeSnifferError.DataAccess(e.error)) else -> Try.failure(MediaTypeSnifferError.NotRecognized) @@ -62,7 +62,7 @@ public class StreamingZipArchiveProvider( } override suspend fun create( - resource: Blob, + resource: Blob, password: String? ): Try, ArchiveFactory.Error> { if (password != null) { @@ -70,15 +70,15 @@ public class StreamingZipArchiveProvider( } return try { - val container = openDataSource( + val container = openBlob( resource, - ::AccessException, + ::ReadException, resource.source ) Try.success(container) } catch (exception: Exception) { - when (val e = exception.unwrapAccessException()) { - is AccessException -> + when (val e = exception.unwrapReadException()) { + is ReadException -> Try.failure(ArchiveFactory.Error.ResourceError(e.error)) else -> Try.failure(ArchiveFactory.Error.ResourceError(ReadError.Content(e))) @@ -86,12 +86,12 @@ public class StreamingZipArchiveProvider( } } - private suspend fun openDataSource( - blob: Blob, + private suspend fun openBlob( + blob: Blob, wrapError: (ReadError) -> IOException, sourceUrl: AbsoluteUrl? ): ClosedContainer = withContext(Dispatchers.IO) { - val datasourceChannel = DatasourceChannel(blob, wrapError) + val datasourceChannel = BlobChannel(blob, wrapError) val channel = wrapBaseChannel(datasourceChannel) val zipFile = ZipFile(channel, true) ChannelZipContainer(zipFile, sourceUrl, mediaTypeRetriever) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt index 1718dfbc2e..fdcc4411db 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt @@ -20,10 +20,10 @@ import org.readium.r2.shared.publication.services.WebPositionsService import org.readium.r2.shared.publication.services.positions import org.readium.r2.shared.publication.services.positionsByReadingOrder import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.StringBlob import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.EmptyContainer import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.StringResource import org.readium.r2.shared.util.resource.readAsString import org.robolectric.RobolectricTestRunner @@ -387,7 +387,7 @@ class PublicationTest { val service = object : Publication.Service { override fun get(href: Url): Resource { assertEquals("link?param1=a¶m2=b", href.toString()) - return StringResource("test passed", MediaType.TEXT) + return StringBlob("test passed", MediaType.TEXT) } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt index 4cb3c70976..f7246808e2 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt @@ -9,16 +9,16 @@ package org.readium.r2.shared.publication.protection 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.data.StringBlob import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry -import org.readium.r2.shared.util.resource.StringResource class TestContainer(resources: Map = emptyMap()) : Container { private val entries: Map = - resources.mapValues { Entry(it.key, StringResource(it.value, MediaType.TEXT)) } + resources.mapValues { Entry(it.key, StringBlob(it.value, MediaType.TEXT)) } override suspend fun entries(): Set = entries.values.toSet() @@ -52,6 +52,6 @@ class TestContainer(resources: Map = emptyMap()) : Container { private class Entry( override val url: Url, - private val resource: StringResource + private val resource: StringBlob ) : Resource by resource, Container.Entry } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/PositionsServiceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/PositionsServiceTest.kt index 4b1b628646..edfd0fbbd2 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/PositionsServiceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/PositionsServiceTest.kt @@ -78,7 +78,7 @@ class PositionsServiceTest { override suspend fun positionsByReadingOrder(): List> = positions } - val json = service.get(Url("/~readium/positions")!!) + val json = service.handle(Url("/~readium/positions")!!) ?.let { runBlocking { it.readAsString() } } ?.getOrNull() ?.let { JSONObject(it) } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIteratorTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIteratorTest.kt index bc8e1c4d19..caca0f3012 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIteratorTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIteratorTest.kt @@ -19,8 +19,8 @@ import org.readium.r2.shared.publication.services.content.Content.TextElement import org.readium.r2.shared.publication.services.content.Content.TextElement.Segment import org.readium.r2.shared.util.Language import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.StringBlob import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.StringResource import org.robolectric.RobolectricTestRunner @OptIn(ExperimentalReadiumApi::class) @@ -182,7 +182,7 @@ class HtmlResourceContentIteratorTest { totalProgressionRange: ClosedRange? = null ): HtmlResourceContentIterator = HtmlResourceContentIterator( - StringResource(html, MediaType.HTML), + StringBlob(html, MediaType.HTML), totalProgressionRange = totalProgressionRange, startLocator ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt index 075a6e3874..a52848b2bf 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt @@ -11,25 +11,25 @@ package org.readium.r2.streamer.extensions import java.io.File import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.readAsXml +import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.readAsXml import org.readium.r2.shared.util.use import org.readium.r2.shared.util.xml.ElementNode /** Returns the resource data as an XML Document at the given [path], or null. */ -internal suspend fun Container.readAsXmlOrNull(path: String): ElementNode? = +internal suspend fun ClosedContainer<*>.readAsXmlOrNull(path: String): ElementNode? = Url.fromDecodedPath(path)?.let { readAsXmlOrNull(it) } /** Returns the resource data as an XML Document at the given [url], or null. */ -internal suspend fun Container.readAsXmlOrNull(url: Url): ElementNode? = - get(url).use { it.readAsXml().getOrNull() } +internal suspend fun ClosedContainer<*>.readAsXmlOrNull(url: Url): ElementNode? = + get(url)?.use { it.readAsXml().getOrNull() } -internal suspend fun Container.guessTitle(): String? { - val entries = entries() ?: return null +internal suspend fun ClosedContainer<*>.guessTitle(): String? { + val entries = entries() val firstEntry = entries.firstOrNull() ?: return null val commonFirstComponent = entries.pathCommonFirstComponent() ?: return null - if (commonFirstComponent.name == firstEntry.url.path) { + if (commonFirstComponent.name == firstEntry.path) { return null } @@ -37,8 +37,8 @@ internal suspend fun Container.guessTitle(): String? { } /** Returns a [File] to the directory containing all paths, if there is such a directory. */ -internal fun Iterable.pathCommonFirstComponent(): File? = - mapNotNull { it.url.path?.substringBefore("/") } +internal fun Iterable.pathCommonFirstComponent(): File? = + mapNotNull { it.path?.substringBefore("/") } .distinct() .takeIf { it.size == 1 } ?.firstOrNull() diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt index 99e674af6a..2cc0574b1f 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt @@ -7,11 +7,23 @@ package org.readium.r2.streamer.extensions import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Container +import org.readium.r2.shared.util.resource.ResourceEntry +import org.readium.r2.shared.util.use -internal suspend fun Container.Entry.toLink(mediaType: MediaType? = null): Link = +internal suspend fun ClosedContainer.linkForUrl( + url: Url, + mediaType: MediaType? = null +): Link = Link( href = url, - mediaType = mediaType ?: mediaType().getOrNull() + mediaType = mediaType ?: get(url)?.use { it.mediaType().getOrNull() } + ) + +internal suspend fun ResourceEntry.toLink(mediaType: MediaType? = null): Link = + Link( + href = url, + mediaType = mediaType ?: this.mediaType().getOrNull() ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index cd92d042aa..409cc3fdc7 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -18,7 +18,7 @@ import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs -import org.readium.r2.streamer.extensions.toLink +import org.readium.r2.streamer.extensions.linkForUrl import org.readium.r2.streamer.parser.PublicationParser /** @@ -64,7 +64,7 @@ public class AudioParser : PublicationParser { conformsTo = setOf(Publication.Profile.AUDIOBOOK), localizedTitle = asset.container.guessTitle()?.let { LocalizedString(it) } ), - readingOrder = readingOrder.map { it.toLink() } + readingOrder = readingOrder.map { asset.container.linkForUrl(it) } ) val publicationBuilder = Publication.Builder( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 4cc9d6af0e..3e264cb28e 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -139,7 +139,10 @@ public class EpubParser( ?.let { EncryptionParser.parse(it) } ?: emptyMap() - private suspend fun parseNavigationData(packageDocument: PackageDocument, container: ClosedContainer): Map> = + private suspend fun parseNavigationData( + packageDocument: PackageDocument, + container: ClosedContainer + ): Map> = parseNavigationDocument(packageDocument, container) ?: parseNcx(packageDocument, container) ?: emptyMap() @@ -156,7 +159,10 @@ public class EpubParser( } ?.takeUnless { it.isEmpty() } - private suspend fun parseNcx(packageDocument: PackageDocument, container: ClosedContainer): Map>? { + private suspend fun parseNcx( + packageDocument: PackageDocument, + container: ClosedContainer + ): Map>? { val ncxItem = if (packageDocument.spine.toc != null) { packageDocument.manifest.firstOrNull { it.id == packageDocument.spine.toc } @@ -187,9 +193,8 @@ public class EpubParser( } private suspend fun ResourceEntry.decodeOrFail( - decode: suspend Resource.() -> Try> + decode: suspend Resource.() -> Try ): Try { - return decode() .mapFailure { when (it) { diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index 91f4ec3f09..ad18859bbc 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -13,12 +13,16 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.PerResourcePositionsService import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.ResourceEntry +import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs -import org.readium.r2.streamer.extensions.toLink +import org.readium.r2.streamer.extensions.linkForUrl import org.readium.r2.streamer.parser.PublicationParser /** @@ -40,12 +44,12 @@ public class ImageParser : PublicationParser { val readingOrder = if (asset.mediaType.matches(MediaType.CBZ)) { (asset.container.entries()) - .filter { !it.isHiddenOrThumbs && it.mediaType().getOrNull()?.isBitmap == true } + .filter { !it.isHiddenOrThumbs && entryIsBitmap(asset.container, it) } .sortedBy { it.toString() } } else { listOfNotNull(asset.container.entries().firstOrNull()) } - .map { it.toLink() } + .map { asset.container.linkForUrl(it) } .toMutableList() if (readingOrder.isEmpty()) { @@ -81,4 +85,7 @@ public class ImageParser : PublicationParser { return Try.success(publicationBuilder) } + + private suspend fun entryIsBitmap(container: ClosedContainer, url: Url) = + container.get(url)!!.use { it.mediaType() }.getOrNull()?.isBitmap == true } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index 990a7ff990..77cd859222 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -16,8 +16,8 @@ import org.readium.r2.shared.publication.services.positionsServiceFactory import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.readAsRwpm import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger @@ -31,7 +31,7 @@ import org.readium.r2.streamer.parser.audio.AudioLocatorService */ public class ReadiumWebPubParser( private val context: Context? = null, - private val pdfFactory: PdfDocumentFactory<*>?, + private val pdfFactory: PdfDocumentFactory<*>? ) : PublicationParser { override suspend fun parse( @@ -62,13 +62,13 @@ public class ReadiumWebPubParser( ) ) } - } ?: return Try.failure( - PublicationParser.Error.ReadError( - ReadError.Content( - MessageError("Missing manifest.") - ) - ) + } ?: return Try.failure( + PublicationParser.Error.ReadError( + ReadError.Content( + MessageError("Missing manifest.") + ) ) + ) // Checks the requirements from the LCPDF specification. // https://readium.org/lcp-specs/notes/lcp-for-pdf.html diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt index e9681ea821..06de2d18a7 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt @@ -11,8 +11,8 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.cover import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.HttpError import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.fetchWithDecoder import org.readium.r2.testapp.utils.tryOrLog From 356cf8a8603b590cd88f5d38380f03e4e81fb306 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Thu, 16 Nov 2023 11:40:29 +0100 Subject: [PATCH 09/86] Introduce HttpStatus --- .../adapter/pdfium/document/PdfiumDocument.kt | 2 +- .../pspdfkit/document/PsPdfKitDocument.kt | 4 +- .../readium/r2/lcp/LcpContentProtection.kt | 4 +- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 10 +-- .../r2/navigator/epub/WebViewServer.kt | 2 +- .../LcpFallbackContentProtection.kt | 6 +- .../services/ContentProtectionService.kt | 10 ++- .../publication/services/CoverService.kt | 4 +- .../util/archive/FileZipArchiveProvider.kt | 2 +- .../readium/r2/shared/util/data/HttpError.kt | 72 ++----------------- .../readium/r2/shared/util/data/HttpStatus.kt | 36 ++++++++++ .../readium/r2/shared/util/data/ReadError.kt | 4 +- .../android/AndroidDownloadManager.kt | 9 ++- .../r2/shared/util/http/DefaultHttpClient.kt | 11 +-- .../readium/r2/shared/util/http/HttpClient.kt | 3 +- .../r2/shared/util/zip/ChannelZipContainer.kt | 2 +- .../util/zip/StreamingZipArchiveProvider.kt | 2 +- .../readium/r2/streamer/ParserAssetFactory.kt | 4 +- .../r2/streamer/parser/audio/AudioParser.kt | 2 +- .../r2/streamer/parser/epub/EpubParser.kt | 10 +-- .../r2/streamer/parser/image/ImageParser.kt | 2 +- .../r2/streamer/parser/pdf/PdfParser.kt | 2 +- .../parser/readium/ReadiumWebPubParser.kt | 8 +-- 23 files changed, 95 insertions(+), 116 deletions(-) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpStatus.kt diff --git a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt index 822d1f4e98..0c218dcd43 100644 --- a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt +++ b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt @@ -116,7 +116,7 @@ public class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory exception.error - else -> ReadError.Content("Pdfium could not read data.") + else -> ReadError.Decoding("Pdfium could not read data.") } Try.failure(error) } diff --git a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt index c416ea0ba8..caf28bdda7 100644 --- a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt @@ -45,11 +45,9 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory FailureResource( - ReadError.Content( + ReadError.Decoding( MessageError( "Cannot decipher content because the publication is locked." ) @@ -185,7 +185,7 @@ internal class LcpDecryptor( if (length < 2 * AES_BLOCK_SIZE) { return Try.failure( - ReadError.Content( + ReadError.Decoding( MessageError("Invalid CBC-encrypted stream.") ) ) @@ -198,7 +198,7 @@ internal class LcpDecryptor( val decryptedBytes = license.decrypt(bytes) .getOrElse { return Try.failure( - ReadError.Content( + ReadError.Decoding( MessageError("Can't decrypt trailing size of CBC-encrypted stream") ) ) @@ -248,7 +248,7 @@ internal class LcpDecryptor( val bytes = license.decrypt(encryptedData) .getOrElse { return Try.failure( - ReadError.Content( + ReadError.Decoding( MessageError( "Can't decrypt the content for resource with key: ${resource.source}", ThrowableError(it) @@ -311,7 +311,7 @@ private suspend fun LcpLicense.decryptFully( var bytes = decrypt(encryptedData) .getOrElse { return Try.failure( - ReadError.Content( + ReadError.Decoding( MessageError("Failed to decrypt the resource", ThrowableError(it)) ) ) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt index 8c006aa6f0..bbc4d5ed2d 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt @@ -98,7 +98,7 @@ internal class WebViewServer( onResourceLoadFailed(urlWithoutAnchor, it) errorResource(urlWithoutAnchor, it) } ?: run { - val error = ReadError.Content( + val error = ReadError.Decoding( "Resource not found at $urlWithoutAnchor in publication." ) onResourceLoadFailed(urlWithoutAnchor, error) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index 845edce97a..f5a86fa5a6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -104,7 +104,7 @@ public class LcpFallbackContentProtection : ContentProtection { ?.getOrElse { when (it) { is DecoderError.DataAccess -> - return Try.failure(ReadError.Content(it)) + return Try.failure(ReadError.Decoding(it)) is DecoderError.DecodingError -> return Try.success(false) } @@ -125,9 +125,9 @@ public class LcpFallbackContentProtection : ContentProtection { ?.getOrElse { when (it) { is DecoderError.DataAccess -> - return Try.failure(ReadError.Content(it.cause.cause)) + return Try.failure(ReadError.Decoding(it.cause.cause)) is DecoderError.DecodingError -> - return Try.failure(ReadError.Content(it.cause)) + return Try.failure(ReadError.Decoding(it.cause)) } } ?: return Try.success(false) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt index 55731d639e..8cd526f713 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt @@ -23,6 +23,7 @@ import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.HttpError +import org.readium.r2.shared.util.data.HttpStatus import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpResponse import org.readium.r2.shared.util.http.HttpStreamResponse @@ -399,16 +400,13 @@ private fun trueResponse(request: HttpRequest): HttpStreamResponse = ) private fun forbiddenResponse(): HttpError.Response = HttpError.Response( - HttpError.Kind.Forbidden, - 403, - null + HttpStatus.Forbidden ) private fun badRequestResponse(detail: String): HttpError.Response = HttpError.Response( - HttpError.Kind.BadRequest, - 400, - null, + HttpStatus.BadRequest, + MediaType.JSON_PROBLEM_DETAILS, JSONObject().apply { put("title", "Bad request") put("detail", detail) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index fb59b87507..4484bc025c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -23,6 +23,7 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.data.HttpError +import org.readium.r2.shared.util.data.HttpStatus import org.readium.r2.shared.util.data.readAsBitmap import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpClient @@ -175,8 +176,7 @@ public abstract class GeneratedCoverService : CoverService, Publication.WebServi val png = cover.toPng() ?: return Try.failure( HttpError.Response( - HttpError.Kind.ServerError, - 500, + HttpStatus(500), null, null ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt index 6531f24f5e..78f1ea1c88 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt @@ -112,7 +112,7 @@ public class FileZipArchiveProvider( } catch (e: ZipException) { Try.failure( ArchiveFactory.Error.ResourceError( - ReadError.Content(e) + ReadError.Decoding(e) ) ) } catch (e: SecurityException) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpError.kt index a4f0824049..9bd0d9e503 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpError.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Readium Foundation. All rights reserved. + * Copyright 2023 Readium Foundation. All rights reserved. * Use of this source code is governed by the BSD-style license * available in the top-level LICENSE file of the project. */ @@ -30,24 +30,23 @@ public sealed class HttpError( public class UnreachableHost(cause: Error) : HttpError("Host could not be reached.", cause) - public class Cancelled(cause: Error) : - HttpError("The request was cancelled.", cause) + public class Redirection(cause: Error) : + HttpError("Redirection failed.", cause) /** An unknown networking error. */ public class Other(cause: Error) : HttpError("A networking error occurred.", cause) - /* - * @param kind Category of HTTP error. + /** + * @param status HTTP status code. * @param mediaType Response media type. * @param body Response body. */ public class Response( - public val kind: Kind, - public val statusCode: Int, + public val status: HttpStatus, public val mediaType: MediaType? = null, public val body: ByteArray? = null - ) : HttpError(kind.message, null) { + ) : HttpError("HTTP Error ${status.code}", null) { /** Response body parsed as a JSON problem details. */ public val problemDetails: ProblemDetails? by lazy { @@ -57,62 +56,5 @@ public sealed class HttpError( tryOrLog { ProblemDetails.fromJSON(JSONObject(String(body))) } } - - public companion object { - - /** - * Creates an HTTP error from a status code. - * - * Returns null if the status code is a success. - */ - public operator fun invoke( - statusCode: Int, - mediaType: MediaType? = null, - body: ByteArray? = null - ): HttpError? = - Kind.ofStatusCode(statusCode)?.let { kind -> - Response(kind, statusCode, mediaType, body) - } - } - } - - public enum class Kind(public val message: String) { - /** (400) The server cannot or will not process the request due to an apparent client error. */ - BadRequest("The provided request was not valid."), - - /** (401) Authentication is required and has failed or has not yet been provided. */ - Unauthorized("Authentication required."), - - /** (403) The server refuses the action, probably because we don't have the necessary permissions. */ - Forbidden("You are not authorized."), - - /** (404) The requested resource could not be found. */ - NotFound("Page not found."), - - /** (405) Method not allowed. */ - MethodNotAllowed("Method not allowed."), - - /** (4xx) Other client errors */ - ClientError("A client error occurred."), - - /** (5xx) Server errors */ - ServerError("A server error occurred, please try again later."); - - public companion object { - - /** Resolves the kind of the HTTP error associated with the given [statusCode]. */ - public fun ofStatusCode(statusCode: Int): Kind? = - when (statusCode) { - in 200..399 -> null - 400 -> BadRequest - 401 -> Unauthorized - 403 -> Forbidden - 404 -> NotFound - 405 -> MethodNotAllowed - in 406..499 -> ClientError - in 500..599 -> ServerError - else -> null - } - } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpStatus.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpStatus.kt new file mode 100644 index 0000000000..5293627740 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpStatus.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +@JvmInline +public value class HttpStatus( + public val code: Int +) : Comparable { + + override fun compareTo(other: HttpStatus): Int = + code.compareTo(other.code) + + public companion object { + + public val Success: HttpStatus = HttpStatus(200) + + /** (400) The server cannot or will not process the request due to an apparent client error. */ + public val BadRequest: HttpStatus = HttpStatus(400) + + /** (401) Authentication is required and has failed or has not yet been provided. */ + public val Unauthorized: HttpStatus = HttpStatus(401) + + /** (403) The server refuses the action, probably because we don't have the necessary permissions. */ + public val Forbidden: HttpStatus = HttpStatus(403) + + /** (404) The requested resource could not be found. */ + public val NotFound: HttpStatus = HttpStatus(404) + + /** (405) Method not allowed. */ + public val MethodNotAllowed: HttpStatus = HttpStatus(405) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt index 0ce09c1b1d..36574c6eef 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt @@ -38,8 +38,8 @@ public sealed class ReadError( public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) } - public class Content(cause: Error? = null) : - ReadError("Content seems invalid. ", cause) { + public class Decoding(cause: Error? = null) : + ReadError("An error occurred while attempting to decode the content.", cause) { public constructor(message: String) : this(MessageError(message)) public constructor(exception: Exception) : this(ThrowableError(exception)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index 320625e224..9a37287dc4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -28,6 +28,7 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.data.HttpError +import org.readium.r2.shared.util.data.HttpStatus import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry @@ -314,7 +315,7 @@ public class AndroidDownloadManager internal constructor( DownloadManager.Error.HttpError(HttpError.MalformedResponse(null)) SystemDownloadManager.ERROR_TOO_MANY_REDIRECTS -> DownloadManager.Error.HttpError( - HttpError.Cancelled(MessageError("Too many redirects.")) + HttpError.Redirection(MessageError("Too many redirects.")) ) SystemDownloadManager.ERROR_CANNOT_RESUME -> DownloadManager.Error.CannotResume() @@ -331,8 +332,10 @@ public class AndroidDownloadManager internal constructor( } private fun httpErrorForCode(code: Int): HttpError = - HttpError.Response(code) - ?: HttpError.MalformedResponse(MessageError("Unknown HTTP status code.")) + when (code) { + in 0 until 1000 -> HttpError.Response(HttpStatus(code)) + else -> HttpError.MalformedResponse(MessageError("Unknown HTTP status code.")) + } public override fun close() { listeners.clear() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index b160a2c167..d199b28a1c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -25,6 +25,7 @@ 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.data.HttpError +import org.readium.r2.shared.util.data.HttpStatus import org.readium.r2.shared.util.data.InMemoryBlob import org.readium.r2.shared.util.e import org.readium.r2.shared.util.flatMap @@ -125,7 +126,7 @@ public class DefaultHttpClient( newRequest: HttpRequest ): HttpTry = Try.failure( - HttpError.Cancelled( + HttpError.Redirection( MessageError("Request cancelled because of an unsafe redirect.") ) ) @@ -160,7 +161,7 @@ public class DefaultHttpClient( var connection = request.toHttpURLConnection() val statusCode = connection.responseCode - HttpError.Kind.ofStatusCode(statusCode)?.let { kind -> + if (statusCode >= 400) { // It was a HEAD request? We need to query the resource again to get the error body. // The body is needed for example when the response is an OPDS Authentication // Document. @@ -182,7 +183,7 @@ public class DefaultHttpClient( ).getOrDefault(MediaType.BINARY) } return@withContext Try.failure( - HttpError.Response(kind, statusCode, mediaType, body) + HttpError.Response(HttpStatus(statusCode), mediaType, body) ) } @@ -216,7 +217,7 @@ public class DefaultHttpClient( return callback.onStartRequest(request) .flatMap { tryStream(it) } .tryRecover { error -> - if (error !is HttpError.Cancelled) { + if (error !is HttpError.Redirection) { callback.onRecoverRequest(request, error) .flatMap { stream(it) } } else { @@ -245,7 +246,7 @@ public class DefaultHttpClient( val redirectCount = request.extras.getInt(EXTRA_REDIRECT_COUNT) if (redirectCount > 5) { return Try.failure( - HttpError.Cancelled( + HttpError.Redirection( MessageError("There were too many redirects to follow.") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt index 55a765da39..85870b6157 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt @@ -16,6 +16,7 @@ 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.data.HttpError +import org.readium.r2.shared.util.data.HttpStatus import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.tryRecover @@ -205,7 +206,7 @@ public suspend fun HttpClient.head(request: HttpRequest): HttpTry .copy { method = HttpRequest.Method.HEAD } .response() .tryRecover { error -> - if (error !is HttpError.Response || error.kind != HttpError.Kind.MethodNotAllowed) { + if (error !is HttpError.Response || error.status != HttpStatus.MethodNotAllowed) { return@tryRecover Try.failure(error) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index 9291d804b2..a962723740 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -98,7 +98,7 @@ internal class ChannelZipContainer( is ReadException -> Try.failure(e.error) else -> - Try.failure(ReadError.Content(e)) + Try.failure(ReadError.Decoding(e)) } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 0170c1ba08..e4ea4ba4ce 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -81,7 +81,7 @@ public class StreamingZipArchiveProvider( is ReadException -> Try.failure(ArchiveFactory.Error.ResourceError(e.error)) else -> - Try.failure(ArchiveFactory.Error.ResourceError(ReadError.Content(e))) + Try.failure(ArchiveFactory.Error.ResourceError(ReadError.Decoding(e))) } } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index 960bbde660..e094b32b42 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -80,7 +80,7 @@ internal class ParserAssetFactory( val manifest = asset.resource.readAsRwpm() .mapFailure { when (it) { - is DecoderError.DecodingError -> ReadError.Content(it.cause) + is DecoderError.DecodingError -> ReadError.Decoding(it.cause) is DecoderError.DataAccess -> it.cause } } @@ -93,7 +93,7 @@ internal class ParserAssetFactory( if (baseUrl !is AbsoluteUrl) { return Try.failure( Error.ReadError( - ReadError.Content("Self link is not absolute.") + ReadError.Decoding("Self link is not absolute.") ) ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index 409cc3fdc7..51af198781 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -52,7 +52,7 @@ public class AudioParser : PublicationParser { if (readingOrder.isEmpty()) { return Try.failure( PublicationParser.Error.ReadError( - ReadError.Content( + ReadError.Decoding( MessageError("No audio file found in the publication.") ) ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 3e264cb28e..ba1d344aa1 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -58,7 +58,7 @@ public class EpubParser( val opfResource = asset.container.get(opfPath) ?: return Try.failure( PublicationParser.Error.ReadError( - ReadError.Content( + ReadError.Decoding( MessageError("Missing OPF file.") ) ) @@ -69,7 +69,7 @@ public class EpubParser( val packageDocument = PackageDocument.parse(opfXmlDocument, opfPath, mediaTypeRetriever) ?: return Try.failure( PublicationParser.Error.ReadError( - ReadError.Content( + ReadError.Decoding( MessageError("Invalid OPF file.") ) ) @@ -115,7 +115,7 @@ public class EpubParser( .get(Url("META-INF/container.xml")!!) ?: return Try.failure( PublicationParser.Error.ReadError( - ReadError.Content("container.xml not found.") + ReadError.Decoding("container.xml not found.") ) ) @@ -129,7 +129,7 @@ public class EpubParser( ?.let { Try.success(it) } ?: Try.failure( PublicationParser.Error.ReadError( - ReadError.Content("Cannot successfully parse OPF.") + ReadError.Decoding("Cannot successfully parse OPF.") ) ) } @@ -202,7 +202,7 @@ public class EpubParser( PublicationParser.Error.ReadError(it.cause) is DecoderError.DecodingError -> PublicationParser.Error.ReadError( - ReadError.Content( + ReadError.Decoding( MessageError( "Couldn't decode resource at $url", it.cause diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index ad18859bbc..a509e089cc 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -55,7 +55,7 @@ public class ImageParser : PublicationParser { if (readingOrder.isEmpty()) { return Try.failure( PublicationParser.Error.ReadError( - ReadError.Content( + ReadError.Decoding( MessageError("No bitmap found in the publication.") ) ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index fc1972a8ef..c9fbb5af77 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -48,7 +48,7 @@ public class PdfParser( ?.let { asset.container.get(it) } ?: return Try.failure( PublicationParser.Error.ReadError( - ReadError.Content( + ReadError.Decoding( MessageError("No PDF found in the publication.") ) ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index 77cd859222..af6c91d3bc 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -50,13 +50,13 @@ public class ReadiumWebPubParser( is DecoderError.DataAccess -> return Try.failure( PublicationParser.Error.ReadError( - ReadError.Content(it.cause) + ReadError.Decoding(it.cause) ) ) is DecoderError.DecodingError -> return Try.failure( PublicationParser.Error.ReadError( - ReadError.Content( + ReadError.Decoding( MessageError("Failed to parse the RWPM Manifest.") ) ) @@ -64,7 +64,7 @@ public class ReadiumWebPubParser( } } ?: return Try.failure( PublicationParser.Error.ReadError( - ReadError.Content( + ReadError.Decoding( MessageError("Missing manifest.") ) ) @@ -78,7 +78,7 @@ public class ReadiumWebPubParser( ) { return Try.failure( PublicationParser.Error.ReadError( - ReadError.Content("Invalid LCP Protected PDF.") + ReadError.Decoding("Invalid LCP Protected PDF.") ) ) } From 14b6f48c70cb1a2ae7681434052a7fbde7906366 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Thu, 16 Nov 2023 12:13:29 +0100 Subject: [PATCH 10/86] Remove entries --- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 21 +++---- .../container/ContainerLicenseContainer.kt | 4 +- .../container/ContentZipLicenseContainer.kt | 4 +- .../lcp/license/container/LicenseContainer.kt | 3 +- .../r2/shared/publication/Publication.kt | 3 +- .../protection/ContentProtection.kt | 4 +- .../publication/services/CoverService.kt | 4 +- .../r2/shared/util/archive/ArchiveProvider.kt | 6 +- .../util/archive/FileZipArchiveProvider.kt | 6 +- .../r2/shared/util/archive/ZipContainer.kt | 9 ++- .../org/readium/r2/shared/util/asset/Asset.kt | 6 +- .../readium/r2/shared/util/data/Container.kt | 19 ++---- .../util/data/RoutingClosedContainer.kt | 4 +- .../r2/shared/util/http/HttpContainer.kt | 9 ++- .../util/resource/DirectoryContainer.kt | 56 +++++++++-------- .../shared/util/resource/ResourceContainer.kt | 61 +++++-------------- .../util/resource/TransformingContainer.kt | 16 ++--- .../r2/shared/util/zip/ChannelZipContainer.kt | 9 ++- .../util/zip/StreamingZipArchiveProvider.kt | 6 +- .../readium/r2/streamer/extensions/Link.kt | 6 +- .../streamer/parser/epub/EpubDeobfuscator.kt | 3 +- .../r2/streamer/parser/epub/EpubParser.kt | 24 ++++---- .../parser/epub/EpubPositionsService.kt | 3 +- .../r2/streamer/parser/image/ImageParser.kt | 4 +- .../r2/streamer/parser/pdf/PdfParser.kt | 8 ++- 25 files changed, 128 insertions(+), 170 deletions(-) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 96c5392363..cb5f9667ed 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -28,7 +28,6 @@ import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.FailureResource import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.resource.TransformingResource import org.readium.r2.shared.util.resource.flatMap import org.readium.r2.shared.util.tryRecover @@ -42,13 +41,9 @@ internal class LcpDecryptor( var encryptionData: Map = emptyMap() ) { - fun transform(resource: Resource): Resource { - if (resource !is ResourceEntry) { - return resource - } - + fun transform(url: Url, resource: Resource): Resource { return resource.flatMap { - val encryption = encryptionData[resource.url] + val encryption = encryptionData[url] // Checks if the resource is encrypted and whether the encryption schemes of the resource // and the DRM license are the same. @@ -67,6 +62,7 @@ internal class LcpDecryptor( ) encryption.isDeflated || !encryption.isCbcEncrypted -> FullLcpResource( + url, resource, encryption, license, @@ -74,6 +70,7 @@ internal class LcpDecryptor( ) else -> CbcLcpResource( + url, resource, encryption, license, @@ -90,7 +87,8 @@ internal class LcpDecryptor( * resource, for example when the resource is deflated before encryption. */ private class FullLcpResource( - private val resource: ResourceEntry, + private val url: Url, + resource: Resource, private val encryption: Encryption, private val license: LcpLicense, private val mediaTypeRetriever: MediaTypeRetriever @@ -101,7 +99,7 @@ internal class LcpDecryptor( override suspend fun mediaType(): Try = mediaTypeRetriever .retrieve( - hints = MediaTypeHints(fileExtension = resource.url.extension), + hints = MediaTypeHints(fileExtension = url.extension), blob = this ) .tryRecover { error -> @@ -127,7 +125,8 @@ internal class LcpDecryptor( * Supports random access for byte range requests, but the resource MUST NOT be deflated. */ private class CbcLcpResource( - private val resource: ResourceEntry, + private val url: Url, + private val resource: Resource, private val encryption: Encryption, private val license: LcpLicense, private val mediaTypeRetriever: MediaTypeRetriever @@ -156,7 +155,7 @@ internal class LcpDecryptor( override suspend fun mediaType(): Try = mediaTypeRetriever .retrieve( - hints = MediaTypeHints(fileExtension = resource.url.extension), + hints = MediaTypeHints(fileExtension = url.extension), blob = this ).tryRecover { error -> when (error) { diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt index e533706c4a..3d60be65f2 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt @@ -11,13 +11,13 @@ import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.getOrThrow -import org.readium.r2.shared.util.resource.ResourceEntry +import org.readium.r2.shared.util.resource.Resource /** * Access to a License Document stored in a read-only container. */ internal class ContainerLicenseContainer( - private val container: ClosedContainer, + private val container: ClosedContainer, private val entryUrl: Url ) : LicenseContainer { diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt index 1fd8177eeb..6556c32bed 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt @@ -18,12 +18,12 @@ import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ClosedContainer -import org.readium.r2.shared.util.resource.ResourceEntry +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.toUri internal class ContentZipLicenseContainer( context: Context, - private val container: ClosedContainer, + private val container: ClosedContainer, private val pathInZip: Url ) : LicenseContainer by ContainerLicenseContainer(container, pathInZip), WritableLicenseContainer { diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt index 66665a21c2..e500c6feb0 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt @@ -18,7 +18,6 @@ import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceEntry private val LICENSE_IN_EPUB = Url("META-INF/license.lcpl")!! private val LICENSE_IN_RPF = Url("license.lcpl")!! @@ -73,7 +72,7 @@ internal fun createLicenseContainer( internal fun createLicenseContainer( context: Context, - container: ClosedContainer, + container: ClosedContainer, mediaType: MediaType ): LicenseContainer { val licensePath = when (mediaType) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index c147f9f264..4fec62f5a5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -40,7 +40,6 @@ import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpStreamResponse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.resource.withMediaType internal typealias ServiceFactory = (Publication.Service.Context) -> Publication.Service? @@ -55,7 +54,7 @@ internal typealias ServiceFactory = (Publication.Service.Context) -> Publication */ public typealias PublicationId = String -public typealias PublicationContainer = ClosedContainer +public typealias PublicationContainer = ClosedContainer /** * The Publication shared model is the entry-point for all the metadata and services diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index 81695e3948..ca8d86056e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -28,7 +28,7 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ResourceEntry +import org.readium.r2.shared.util.resource.Resource /** * Bridge between a Content Protection technology and the Readium toolkit. @@ -85,7 +85,7 @@ public interface ContentProtection { */ public data class Asset( val mediaType: MediaType, - val container: ClosedContainer, + val container: ClosedContainer, val onCreatePublication: Publication.Builder.() -> Unit = {} ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index 4484bc025c..ce4511d50b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -32,7 +32,7 @@ import org.readium.r2.shared.util.http.HttpResponse import org.readium.r2.shared.util.http.HttpStreamResponse import org.readium.r2.shared.util.http.fetch import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ResourceEntry +import org.readium.r2.shared.util.resource.Resource /** * Provides an easy access to a bitmap version of the publication cover. @@ -127,7 +127,7 @@ internal class ExternalCoverService( internal class ResourceCoverService( private val coverUrl: Url, - private val container: ClosedContainer + private val container: ClosedContainer ) : CoverService { override suspend fun cover(): Bitmap? { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt index 33317ef7c8..667f589be0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt @@ -14,7 +14,7 @@ import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaTypeSniffer import org.readium.r2.shared.util.resource.ResourceContainer -import org.readium.r2.shared.util.resource.ResourceEntry +import org.readium.r2.shared.util.resource.Resource public interface ArchiveProvider : MediaTypeSniffer, ArchiveFactory @@ -51,7 +51,7 @@ public interface ArchiveFactory { public suspend fun create( resource: Blob, password: String? = null - ): Try, Error> + ): Try, Error> } public class CompositeArchiveFactory( @@ -63,7 +63,7 @@ public class CompositeArchiveFactory( override suspend fun create( resource: Blob, password: String? - ): Try, ArchiveFactory.Error> { + ): Try, ArchiveFactory.Error> { for (factory in factories) { factory.create(resource, password) .getOrElse { error -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt index 78f1ea1c88..8bb16cd4e1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt @@ -25,7 +25,7 @@ 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.resource.ResourceEntry +import org.readium.r2.shared.util.resource.Resource /** * An [ArchiveFactory] to open local ZIP files with Java's [ZipFile]. @@ -79,7 +79,7 @@ public class FileZipArchiveProvider( override suspend fun create( resource: Blob, password: String? - ): Try, ArchiveFactory.Error> { + ): Try, ArchiveFactory.Error> { if (password != null) { return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) } @@ -98,7 +98,7 @@ public class FileZipArchiveProvider( } // Internal for testing purpose - internal suspend fun open(file: File): Try, ArchiveFactory.Error> = + internal suspend fun open(file: File): Try, ArchiveFactory.Error> = withContext(Dispatchers.IO) { try { val archive = JavaZipContainer(ZipFile(file), file, mediaTypeRetriever) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt index a243711f0c..b06746e548 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt @@ -28,7 +28,6 @@ import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.tryRecover @@ -36,10 +35,10 @@ internal class JavaZipContainer( private val archive: ZipFile, file: File, private val mediaTypeRetriever: MediaTypeRetriever -) : ClosedContainer { +) : ClosedContainer { - private inner class Entry(override val url: Url, private val entry: ZipEntry) : - ResourceEntry { + private inner class Entry(private val url: Url, private val entry: ZipEntry) : + Resource { override val source: AbsoluteUrl? = null @@ -147,7 +146,7 @@ internal class JavaZipContainer( .mapNotNull { entry -> Url.fromDecodedPath(entry.name) } .toSet() - override fun get(url: Url): ResourceEntry? = + override fun get(url: Url): Resource? = (url as? RelativeUrl)?.path ?.let { tryOrLog { archive.getEntry(it) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt index 0118805f8d..0c51e494dd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt @@ -8,8 +8,6 @@ package org.readium.r2.shared.util.asset import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Resource as SharedResource -import org.readium.r2.shared.util.resource.ResourceEntry /** * An asset which is either a single resource or a container that holds multiple resources. @@ -34,7 +32,7 @@ public sealed class Asset { */ public class Resource( override val mediaType: MediaType, - public val resource: SharedResource + public val resource: org.readium.r2.shared.util.resource.Resource ) : Asset() { override suspend fun close() { @@ -52,7 +50,7 @@ public sealed class Asset { public class Container( override val mediaType: MediaType, public val containerType: MediaType, - public val container: ClosedContainer + public val container: ClosedContainer ) : Asset() { override suspend fun close() { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index 00739f0bc7..3dd45c0e44 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -12,21 +12,10 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.resource.Resource -/** - * Represents a container entry's. - */ -public interface ContainerEntry : Blob { - - /** - * URL used to access the resource in the container. - */ - public val url: Url -} - /** * A container provides access to a list of [Resource] entries. */ -public interface Container : SuspendingCloseable { +public interface Container : SuspendingCloseable { /** * Direct source to this container, when available. @@ -42,7 +31,7 @@ public interface Container : SuspendingCloseable { public fun get(url: Url): E? } -public interface ClosedContainer : Container { +public interface ClosedContainer : Container { /** * List of all the container entries. @@ -51,7 +40,7 @@ public interface ClosedContainer : Container { } /** A [Container] providing no resources at all. */ -public class EmptyContainer : ClosedContainer { +public class EmptyContainer : ClosedContainer { override suspend fun entries(): Set = emptySet() @@ -63,7 +52,7 @@ public class EmptyContainer : ClosedContainer { /** * Returns whether an entry exists in the container. */ -internal suspend fun Container.contains(url: Url): Try { +internal suspend fun Container.contains(url: Url): Try { if (this is ClosedContainer) { return Try.success(url in entries()) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingClosedContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingClosedContainer.kt index fbcca7b271..eeaa2de181 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingClosedContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingClosedContainer.kt @@ -17,7 +17,7 @@ import org.readium.r2.shared.util.Url * * The [routes] will be tested in the given order. */ -public class RoutingClosedContainer( +public class RoutingClosedContainer( private val routes: List> ) : ClosedContainer { @@ -26,7 +26,7 @@ public class RoutingClosedContainer( * * The default value for [accepts] means that the fetcher will accept any link. */ - public class Route( + public class Route( public val container: ClosedContainer, public val accepts: (Url) -> Boolean = { true } ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt index 0c31423505..337e134238 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt @@ -9,8 +9,7 @@ package org.readium.r2.shared.util.http import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ClosedContainer -import org.readium.r2.shared.util.resource.ResourceEntry -import org.readium.r2.shared.util.resource.toResourceEntry +import org.readium.r2.shared.util.resource.Resource /** * Fetches remote resources through HTTP. @@ -25,17 +24,17 @@ public class HttpContainer( private val client: HttpClient, private val baseUrl: Url? = null, private val entries: Set -) : ClosedContainer { +) : ClosedContainer { override suspend fun entries(): Set = entries - override fun get(url: Url): ResourceEntry? { + override fun get(url: Url): Resource? { val absoluteUrl = (baseUrl?.resolve(url) ?: url) as? AbsoluteUrl return if (absoluteUrl == null || !absoluteUrl.isHttp) { null } else { - HttpResource(client, absoluteUrl).toResourceEntry(url) + HttpResource(client, absoluteUrl) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index 893b07c72c..7db4835724 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -10,12 +10,13 @@ import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.isParentOf -import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.util.RelativeUrl +import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.FileBlob +import org.readium.r2.shared.util.data.FilesystemError import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.toUrl @@ -25,43 +26,46 @@ import org.readium.r2.shared.util.toUrl */ public class DirectoryContainer( private val root: File, - private val mediaTypeRetriever: MediaTypeRetriever -) : ClosedContainer { + private val mediaTypeRetriever: MediaTypeRetriever, + private val entries: Set +) : ClosedContainer { - private val _entries: Set by lazy { - tryOrNull { - root.walk() - .filter { it.isFile } - .mapNotNull { it.toUrl() } - .toSet() - }.orEmpty() - } - - private fun File.toEntry(): ResourceEntry? { - val url = Url.fromDecodedPath(this.relativeTo(root).path) - ?: return null - - val resource = GuessMediaTypeResourceAdapter( + private fun File.toResource(): Resource { + return GuessMediaTypeResourceAdapter( FileBlob(this), mediaTypeRetriever, MediaTypeHints(fileExtension = extension) ) - return DelegatingResourceEntry(url, resource) } override suspend fun entries(): Set { - return withContext(Dispatchers.IO) { - _entries - } + return entries } - override fun get(url: Url): ResourceEntry? { - val file = (url as? RelativeUrl)?.path + override fun get(url: Url): Resource? = + (url as? RelativeUrl)?.path ?.let { File(root, it) } ?.takeIf { !root.isParentOf(it) } - - return file?.toEntry() - } + ?.toResource() override suspend fun close() {} + + public companion object { + + public suspend operator fun invoke(root: File, mediaTypeRetriever: MediaTypeRetriever): Try { + val entries = + try { + withContext(Dispatchers.IO) { + root.walk() + .filter { it.isFile } + .map { it.toUrl() } + .toSet() + } + } catch (e: SecurityException) { + return Try.failure(FilesystemError.Forbidden(e)) + } + val container = DirectoryContainer(root, mediaTypeRetriever, entries) + return Try.success(container) + } + } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt index c5527cc18d..d73205ec30 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt @@ -6,73 +6,42 @@ package org.readium.r2.shared.util.resource -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.data.ClosedContainer import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.ContainerEntry import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.mediatype.MediaType public typealias ResourceTry = Try -public interface ResourceEntry : ContainerEntry, Resource - -public typealias ResourceContainer = Container - -public class FailureResourceEntry( - override val url: Url, - private val error: ReadError -) : ResourceEntry { - - override val source: AbsoluteUrl? = null - - override suspend fun mediaType(): ResourceTry = - Try.failure(error) - - override suspend fun properties(): ResourceTry = - Try.failure(error) - - override suspend fun length(): ResourceTry = - Try.failure(error) - - override suspend fun read(range: LongRange?): ResourceTry = - Try.failure(error) - - override suspend fun close() { - } -} +public typealias ResourceContainer = Container /** A [Container] for a single [Resource]. */ public class SingleResourceContainer( private val url: Url, - resource: Resource -) : ClosedContainer { - public interface Entry : ResourceEntry + private val resource: Resource +) : ClosedContainer { - private val entry = resource.toResourceEntry(url) + private class Entry( + private val resource: Resource + ) : Resource by resource { + + override suspend fun close() { + // Do nothing + } + } override suspend fun entries(): Set = setOf(url) - override fun get(url: Url): ResourceEntry? { - if (url.removeFragment().removeQuery() != entry.url) { + override fun get(url: Url): Resource? { + if (url.removeFragment().removeQuery() != url) { return null } - return entry + return Entry(resource) } override suspend fun close() { - entry.close() + resource.close() } } - -public class DelegatingResourceEntry( - override val url: Url, - private val resource: Resource -) : ResourceEntry, Resource by resource - -/** Convenience helper to wrap a [Resource] and a [url] into a [Container.Entry]. */ -internal fun Resource.toResourceEntry(url: Url): ResourceEntry = - DelegatingResourceEntry(url, this) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt index 6357322c77..214e4dfb4d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt @@ -23,24 +23,24 @@ public typealias ResourceTransformer = (Resource) -> Resource * functions. */ public class TransformingContainer( - private val container: ClosedContainer, - private val transformers: List -) : ClosedContainer { + private val container: ClosedContainer, + private val transformers: List<(Url, Resource) -> Resource> +) : ClosedContainer { - public constructor(container: ClosedContainer, transformer: ResourceTransformer) : + public constructor(container: ClosedContainer, transformer: (Url, Resource) -> Resource) : this(container, listOf(transformer)) override suspend fun entries(): Set = container.entries() - override fun get(url: Url): ResourceEntry? { + override fun get(url: Url): Resource? { val originalResource = container.get(url) ?: return null return transformers - .fold(originalResource) { acc: Resource, transformer: ResourceTransformer -> - transformer(acc) - }.toResourceEntry(url) + .fold(originalResource) { acc: Resource, transformer: (Url, Resource) -> Resource -> + transformer(url, acc) + } } override suspend fun close() { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index a962723740..b438f6ded9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -27,7 +27,6 @@ import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.tryRecover import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipArchiveEntry @@ -37,12 +36,12 @@ internal class ChannelZipContainer( private val zipFile: ZipFile, override val source: AbsoluteUrl?, private val mediaTypeRetriever: MediaTypeRetriever -) : ClosedContainer { +) : ClosedContainer { private inner class Entry( - override val url: Url, + private val url: Url, private val entry: ZipArchiveEntry - ) : ResourceEntry { + ) : Resource { override val source: AbsoluteUrl? get() = null @@ -158,7 +157,7 @@ internal class ChannelZipContainer( .mapNotNull { entry -> Url.fromDecodedPath(entry.name) } .toSet() - override fun get(url: Url): ResourceEntry? = + override fun get(url: Url): Resource? = (url as? RelativeUrl)?.path ?.let { zipFile.getEntry(it) } ?.takeUnless { it.isDirectory } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index e4ea4ba4ce..0d4a705d31 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -24,7 +24,7 @@ import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.ResourceContainer -import org.readium.r2.shared.util.resource.ResourceEntry +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel @@ -64,7 +64,7 @@ public class StreamingZipArchiveProvider( override suspend fun create( resource: Blob, password: String? - ): Try, ArchiveFactory.Error> { + ): Try, ArchiveFactory.Error> { if (password != null) { return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) } @@ -90,7 +90,7 @@ public class StreamingZipArchiveProvider( blob: Blob, wrapError: (ReadError) -> IOException, sourceUrl: AbsoluteUrl? - ): ClosedContainer = withContext(Dispatchers.IO) { + ): ClosedContainer = withContext(Dispatchers.IO) { val datasourceChannel = BlobChannel(blob, wrapError) val channel = wrapBaseChannel(datasourceChannel) val zipFile = ZipFile(channel, true) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt index 2cc0574b1f..702f237d65 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt @@ -10,10 +10,10 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ResourceEntry +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use -internal suspend fun ClosedContainer.linkForUrl( +internal suspend fun ClosedContainer.linkForUrl( url: Url, mediaType: MediaType? = null ): Link = @@ -22,7 +22,7 @@ internal suspend fun ClosedContainer.linkForUrl( mediaType = mediaType ?: get(url)?.use { it.mediaType().getOrNull() } ) -internal suspend fun ResourceEntry.toLink(mediaType: MediaType? = null): Link = +internal suspend fun Resource.toLink(url: Url, mediaType: MediaType? = null): Link = Link( href = url, mediaType = mediaType ?: this.mediaType().getOrNull() diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt index c102d9a4bb..64c82679c2 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt @@ -24,7 +24,8 @@ internal class EpubDeobfuscator( private val retrieveEncryption: (Url) -> Encryption? ) { - fun transform(resource: Resource): Resource = + @Suppress("Unused_parameter") + fun transform(url: Url, resource: Resource): Resource = resource.flatMap { val algorithm = resource.source?.let(retrieveEncryption)?.algorithm if (algorithm != null && algorithm2length.containsKey(algorithm)) { diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index ba1d344aa1..0602ce7fa7 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -26,7 +26,6 @@ import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.resource.TransformingContainer import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.readAsXmlOrNull @@ -64,7 +63,7 @@ public class EpubParser( ) ) val opfXmlDocument = opfResource - .use { it.decodeOrFail { readAsXml() } } + .use { it.decodeOrFail(opfPath) { readAsXml() } } .getOrElse { return Try.failure(it) } val packageDocument = PackageDocument.parse(opfXmlDocument, opfPath, mediaTypeRetriever) ?: return Try.failure( @@ -110,9 +109,11 @@ public class EpubParser( return Try.success(builder) } - private suspend fun getRootFilePath(container: ClosedContainer): Try { + private suspend fun getRootFilePath(container: ClosedContainer): Try { + val containerXmlUrl =Url("META-INF/container.xml")!! + val containerXmlResource = container - .get(Url("META-INF/container.xml")!!) + .get(containerXmlUrl) ?: return Try.failure( PublicationParser.Error.ReadError( ReadError.Decoding("container.xml not found.") @@ -120,7 +121,7 @@ public class EpubParser( ) return containerXmlResource - .use { it.decodeOrFail { readAsXml() } } + .use { it.decodeOrFail(containerXmlUrl) { readAsXml() } } .getOrElse { return Try.failure(it) } .getFirst("rootfiles", Namespaces.OPC) ?.getFirst("rootfile", Namespaces.OPC) @@ -134,14 +135,14 @@ public class EpubParser( ) } - private suspend fun parseEncryptionData(container: ClosedContainer): Map = + private suspend fun parseEncryptionData(container: ClosedContainer): Map = container.readAsXmlOrNull("META-INF/encryption.xml") ?.let { EncryptionParser.parse(it) } ?: emptyMap() private suspend fun parseNavigationData( packageDocument: PackageDocument, - container: ClosedContainer + container: ClosedContainer ): Map> = parseNavigationDocument(packageDocument, container) ?: parseNcx(packageDocument, container) @@ -149,7 +150,7 @@ public class EpubParser( private suspend fun parseNavigationDocument( packageDocument: PackageDocument, - container: ClosedContainer + container: ClosedContainer ): Map>? = packageDocument.manifest .firstOrNull { it.properties.contains(Vocabularies.ITEM + "nav") } @@ -161,7 +162,7 @@ public class EpubParser( private suspend fun parseNcx( packageDocument: PackageDocument, - container: ClosedContainer + container: ClosedContainer ): Map>? { val ncxItem = if (packageDocument.spine.toc != null) { @@ -177,7 +178,7 @@ public class EpubParser( ?.takeUnless { it.isEmpty() } } - private suspend fun parseDisplayOptions(container: ClosedContainer): Map { + private suspend fun parseDisplayOptions(container: ClosedContainer): Map { val displayOptionsXml = container.readAsXmlOrNull("META-INF/com.apple.ibooks.display-options.xml") ?: container.readAsXmlOrNull("META-INF/com.kobobooks.display-options.xml") @@ -192,7 +193,8 @@ public class EpubParser( ?.toMap().orEmpty() } - private suspend fun ResourceEntry.decodeOrFail( + private suspend fun Resource.decodeOrFail( + url: Url, decode: suspend Resource.() -> Try ): Try { return decode() diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt index 7b13f1c5f2..bb9fde83c7 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt @@ -21,7 +21,6 @@ import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceEntry import org.readium.r2.shared.util.use /** @@ -36,7 +35,7 @@ import org.readium.r2.shared.util.use public class EpubPositionsService( private val readingOrder: List, private val presentation: Presentation, - private val container: ClosedContainer, + private val container: ClosedContainer, private val reflowableStrategy: ReflowableStrategy ) : PositionsService { diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index a509e089cc..32466b633d 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -18,7 +18,7 @@ import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ResourceEntry +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs @@ -86,6 +86,6 @@ public class ImageParser : PublicationParser { return Try.success(publicationBuilder) } - private suspend fun entryIsBitmap(container: ClosedContainer, url: Url) = + private suspend fun entryIsBitmap(container: ClosedContainer, url: Url) = container.get(url)!!.use { it.mediaType() }.getOrNull()?.isBitmap == true } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index c9fbb5af77..632fc95174 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -43,8 +43,10 @@ public class PdfParser( return Try.failure(PublicationParser.Error.UnsupportedFormat()) } - val resource = asset.container.entries() + val url = asset.container.entries() .firstOrNull() + + val resource = url ?.let { asset.container.get(it) } ?: return Try.failure( PublicationParser.Error.ReadError( @@ -55,7 +57,7 @@ public class PdfParser( ) val document = pdfFactory.open(resource, password = null) .getOrElse { return Try.failure(PublicationParser.Error.ReadError(it)) } - val tableOfContents = document.outline.toLinks(resource.url) + val tableOfContents = document.outline.toLinks(url) val manifest = Manifest( metadata = Metadata( @@ -66,7 +68,7 @@ public class PdfParser( readingProgression = document.readingProgression, numberOfPages = document.pageCount ), - readingOrder = listOf(resource.toLink(MediaType.PDF)), + readingOrder = listOf(resource.toLink(url, MediaType.PDF)), tableOfContents = tableOfContents ) From 06d64f7bd4401c4fc29e09dfecef11f458caa509 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Fri, 17 Nov 2023 09:10:36 +0100 Subject: [PATCH 11/86] Get rid of Other errors --- .../pspdfkit/document/PsPdfKitDocument.kt | 11 +-- .../pspdfkit/document/ResourceDataProvider.kt | 5 ++ .../r2/navigator/media/ExoMediaPlayer.kt | 4 +- .../media/tts/session/TtsSessionAdapter.kt | 4 +- .../r2/shared/publication/Publication.kt | 2 +- .../services/ContentProtectionService.kt | 4 +- .../publication/services/CoverService.kt | 4 +- .../publication/services/PositionsService.kt | 2 +- .../services/search/SearchService.kt | 2 +- .../util/archive/FileZipArchiveProvider.kt | 25 ++---- .../r2/shared/util/archive/ZipContainer.kt | 16 ++-- .../r2/shared/util/data/ContentBlob.kt | 32 +++++--- .../shared/util/data/ContentProviderError.kt | 37 +++++++++ .../readium/r2/shared/util/data/FileBlob.kt | 15 ++-- ...{FilesystemError.kt => FileSystemError.kt} | 10 +-- .../readium/r2/shared/util/data/ReadError.kt | 26 +++--- ...ClosedContainer.kt => RoutingContainer.kt} | 2 +- .../shared/util/downloads/DownloadManager.kt | 2 +- .../android/AndroidDownloadManager.kt | 4 +- .../foreground/ForegroundDownloadManager.kt | 8 +- .../r2/shared/util/http/DefaultHttpClient.kt | 14 ++-- .../readium/r2/shared/util/http/HttpClient.kt | 79 +----------------- .../shared/util/{data => http}/HttpError.kt | 11 ++- .../r2/shared/util/http/HttpResource.kt | 18 +++-- .../r2/shared/util/http/HttpResponse.kt | 80 +++++++++++++++++++ .../shared/util/{data => http}/HttpStatus.kt | 2 +- .../util/resource/DirectoryContainer.kt | 6 +- .../r2/shared/util/zip/ChannelZipContainer.kt | 7 +- .../readium/r2/streamer/ParserAssetFactory.kt | 4 +- .../readium/r2/testapp/domain/CoverStorage.kt | 2 +- 30 files changed, 253 insertions(+), 185 deletions(-) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/data/{FilesystemError.kt => FileSystemError.kt} (75%) rename readium/shared/src/main/java/org/readium/r2/shared/util/data/{RoutingClosedContainer.kt => RoutingContainer.kt} (97%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{data => http}/HttpError.kt (86%) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/{data => http}/HttpStatus.kt (96%) diff --git a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt index caf28bdda7..3a4f8ec0a9 100644 --- a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt @@ -6,17 +6,18 @@ package org.readium.adapter.pspdfkit.document +import com.pspdfkit.document.PdfDocument as _PsPdfKitDocument import android.content.Context import android.graphics.Bitmap import com.pspdfkit.annotations.actions.GoToAction import com.pspdfkit.document.DocumentSource import com.pspdfkit.document.OutlineElement import com.pspdfkit.document.PageBinding -import com.pspdfkit.document.PdfDocument as _PsPdfKitDocument import com.pspdfkit.document.PdfDocumentLoader import com.pspdfkit.exceptions.InvalidPasswordException +import com.pspdfkit.exceptions.InvalidSignatureException +import java.io.IOException import kotlin.reflect.KClass -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.publication.ReadingProgression @@ -46,9 +47,9 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory Unit = { Timber.e(it) } ) : DataProvider { + var error: ReadError? = null + private val resource = // PSPDFKit accesses the resource from multiple threads. resource.synchronized() @@ -30,6 +32,7 @@ internal class ResourceDataProvider( runBlocking { resource.length() .getOrElse { + error = it onResourceError(it) DataProvider.FILE_SIZE_UNKNOWN.toLong() } @@ -51,6 +54,7 @@ internal class ResourceDataProvider( val range = offset until (offset + size) resource.read(range) .getOrElse { + error = it onResourceError(it) DataProvider.NO_DATA_AVAILABLE } @@ -58,6 +62,7 @@ internal class ResourceDataProvider( override fun release() { if (::resource.isLazyInitialized) { + error = null runBlocking { resource.close() } } } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt index 590c26c19c..8ea2038801 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt @@ -56,7 +56,7 @@ import org.readium.r2.shared.publication.PublicationId import org.readium.r2.shared.publication.indexOfFirstWithHref import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.HttpError +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.toUri import timber.log.Timber @@ -204,7 +204,7 @@ public class ExoMediaPlayer( override fun onPlayerError(error: PlaybackException) { var resourceError: ReadError? = error.asInstance() if (resourceError == null && (error.cause as? HttpDataSource.HttpDataSourceException)?.cause is UnknownHostException) { - resourceError = ReadError.Network( + resourceError = ReadError.Access( HttpError.UnreachableHost(ThrowableError(error.cause!!)) ) } diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt index 874d708562..2755d96fad 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt @@ -44,7 +44,7 @@ import org.readium.navigator.media.tts.TtsEngine import org.readium.navigator.media.tts.TtsPlayer import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.ErrorException -import org.readium.r2.shared.util.data.HttpError +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.data.ReadError /** @@ -925,7 +925,7 @@ internal class TtsSessionAdapter( } is TtsPlayer.State.Error.ContentError -> { val errorCode = when (error) { - is ReadError.Network -> + is ReadError.Access -> when (error.cause) { is HttpError.Response -> ERROR_CODE_IO_BAD_HTTP_STATUS diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index 4fec62f5a5..26a687ad61 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -34,7 +34,7 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.data.EmptyContainer -import org.readium.r2.shared.util.data.HttpError +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpStreamResponse diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt index 8cd526f713..faec271212 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt @@ -22,8 +22,8 @@ import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.HttpError -import org.readium.r2.shared.util.data.HttpStatus +import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpResponse import org.readium.r2.shared.util.http.HttpStreamResponse diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index ce4511d50b..bcbdd8cc57 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -22,8 +22,8 @@ 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.data.ClosedContainer -import org.readium.r2.shared.util.data.HttpError -import org.readium.r2.shared.util.data.HttpStatus +import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.shared.util.data.readAsBitmap import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpClient diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt index dba12e839a..f606ef3170 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt @@ -25,7 +25,7 @@ import org.readium.r2.shared.toJSON 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.data.HttpError +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpResponse diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt index eee764c2db..d97f5e0255 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.HttpError +import org.readium.r2.shared.util.http.HttpError @ExperimentalReadiumApi public typealias SearchTry = Try diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt index 8bb16cd4e1..5f2dc05e2b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt @@ -14,11 +14,10 @@ import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.util.MessageError -import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.ClosedContainer -import org.readium.r2.shared.util.data.FilesystemError +import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType @@ -57,19 +56,13 @@ public class FileZipArchiveProvider( } catch (e: SecurityException) { Try.failure( MediaTypeSnifferError.DataAccess( - ReadError.Filesystem(FilesystemError.Forbidden(e)) + ReadError.Access(FileSystemError.Forbidden(e)) ) ) } catch (e: IOException) { Try.failure( MediaTypeSnifferError.DataAccess( - ReadError.Filesystem(FilesystemError.Unknown(e)) - ) - ) - } catch (e: Exception) { - Try.failure( - MediaTypeSnifferError.DataAccess( - ReadError.Other(ThrowableError(e)) + ReadError.Access(FileSystemError.IO(e)) ) ) } @@ -106,7 +99,7 @@ public class FileZipArchiveProvider( } catch (e: FileNotFoundException) { Try.failure( ArchiveFactory.Error.ResourceError( - ReadError.Filesystem(FilesystemError.NotFound(e)) + ReadError.Access(FileSystemError.NotFound(e)) ) ) } catch (e: ZipException) { @@ -118,19 +111,13 @@ public class FileZipArchiveProvider( } catch (e: SecurityException) { Try.failure( ArchiveFactory.Error.ResourceError( - ReadError.Filesystem(FilesystemError.Forbidden(e)) + ReadError.Access(FileSystemError.Forbidden(e)) ) ) } catch (e: IOException) { Try.failure( ArchiveFactory.Error.ResourceError( - ReadError.Filesystem(FilesystemError.Unknown(e)) - ) - ) - } catch (e: Exception) { - Try.failure( - ArchiveFactory.Error.ResourceError( - ReadError.Other(e) + ReadError.Access(FileSystemError.IO(e)) ) ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt index b06746e548..5c072aa920 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt @@ -9,17 +9,19 @@ package org.readium.r2.shared.util.archive import java.io.File import java.io.IOException import java.util.zip.ZipEntry +import java.util.zip.ZipException import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.readFully import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ClosedContainer -import org.readium.r2.shared.util.data.FilesystemError +import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream @@ -69,7 +71,11 @@ internal class JavaZipContainer( override suspend fun length(): Try = entry.size.takeUnless { it == -1L } ?.let { Try.success(it) } - ?: Try.failure(ReadError.Other(Exception("Unsupported operation"))) + ?: Try.failure( + ReadError.UnsupportedOperation( + MessageError("ZIP entry doesn't provide length for entry $url.") + ) + ) private val compressedLength: Long? = if (entry.method == ZipEntry.STORED || entry.method == -1) { @@ -89,10 +95,10 @@ internal class JavaZipContainer( } Try.success(bytes) } + } catch (e: ZipException) { + Try.failure(ReadError.Decoding(e)) } catch (e: IOException) { - Try.failure(ReadError.Filesystem(FilesystemError.Unknown(e))) - } catch (e: Exception) { - Try.failure(ReadError.Other(e)) + Try.failure(ReadError.Access(FileSystemError.IO(e))) } private suspend fun readFully(): ByteArray = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt index be1000cd8d..c2a114a22d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt @@ -16,7 +16,9 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.toUrl /** @@ -57,8 +59,15 @@ public class ContentBlob( private suspend fun readRange(range: LongRange): Try = withStream { withContext(Dispatchers.IO) { - val skipped = it.skip(range.first) - check(skipped == range.first) + var skipped: Long = 0 + + while (skipped != range.first) { + skipped += it.skip(range.first - skipped) + if (skipped == 0L) { + throw IOException("Could not skip InputStream.") + } + } + val length = range.last - range.first + 1 it.read(length) } @@ -68,7 +77,16 @@ public class ContentBlob( if (!::_length.isInitialized) { _length = Try.catching { contentResolver.openFileDescriptor(uri, "r") - .use { fd -> checkNotNull(fd?.statSize.takeUnless { it == -1L }) } + ?.use { fd -> fd.statSize.takeUnless { it == -1L } } + }.flatMap { + when (it) { + null -> Try.failure( + ReadError.UnsupportedOperation( + MessageError("Content provider does not provide length for uri $uri.") + ) + ) + else -> Try.success(it) + } } } @@ -93,13 +111,9 @@ public class ContentBlob( try { success(closure()) } catch (e: FileNotFoundException) { - failure(ReadError.Filesystem(FilesystemError.NotFound(e))) - } catch (e: SecurityException) { - failure(ReadError.Filesystem(FilesystemError.Forbidden(e))) + failure(ReadError.Access(ContentProviderError.FileNotFound(e))) } catch (e: IOException) { - failure(ReadError.Filesystem(FilesystemError.Unknown(e))) - } catch (e: Exception) { - failure(ReadError.Filesystem(FilesystemError.Unknown(e))) + failure(ReadError.Access(ContentProviderError.IO(e))) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. failure(ReadError.OutOfMemory(e)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt new file mode 100644 index 0000000000..2e5430f50a --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ThrowableError + +public sealed class ContentProviderError( + override val message: String, + override val cause: Error? = null +) : Error { + + public class FileNotFound( + cause: Error? + ) : FileSystemError("File not found.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + public class NotAvailable( + cause: Error? + ) : FileSystemError("Content Provider recently crashed.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } + + public class IO( + cause: Error? + ) : FileSystemError("An IO error occurred.", cause) { + + public constructor(exception: Exception) : this(ThrowableError(exception)) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt index aa56e458cb..43b393773e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.isLazyInitialized @@ -82,7 +83,11 @@ public class FileBlob( override suspend fun length(): Try = metadataLength?.let { Try.success(it) } - ?: read().map { it.size.toLong() } + ?: Try.failure( + ReadError.UnsupportedOperation( + MessageError("Length not available for file at ${file.path}.") + ) + ) private val metadataLength: Long? = tryOrNull { @@ -97,13 +102,13 @@ public class FileBlob( try { success(closure()) } catch (e: FileNotFoundException) { - failure(ReadError.Filesystem(FilesystemError.NotFound(e))) + failure(ReadError.Access(FileSystemError.NotFound(e))) } catch (e: SecurityException) { - failure(ReadError.Filesystem(FilesystemError.Forbidden(e))) + failure(ReadError.Access(FileSystemError.Forbidden(e))) } catch (e: IOException) { - failure(ReadError.Filesystem(FilesystemError.Unknown(e))) + failure(ReadError.Access(FileSystemError.IO(e))) } catch (e: Exception) { - failure(ReadError.Filesystem(FilesystemError.Unknown(e))) + failure(ReadError.Access(FileSystemError.IO(e))) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. failure(ReadError.OutOfMemory(e)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FilesystemError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt similarity index 75% rename from readium/shared/src/main/java/org/readium/r2/shared/util/data/FilesystemError.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt index b7ed6bcaa0..073fdf27a8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FilesystemError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt @@ -9,28 +9,28 @@ package org.readium.r2.shared.util.data import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ThrowableError -public sealed class FilesystemError( +public sealed class FileSystemError( override val message: String, override val cause: Error? = null ) : Error { public class NotFound( cause: Error? - ) : FilesystemError("File not found.", cause) { + ) : FileSystemError("File not found.", cause) { public constructor(exception: Exception) : this(ThrowableError(exception)) } public class Forbidden( cause: Error? - ) : FilesystemError("You are not allowed to access this file.", cause) { + ) : FileSystemError("You are not allowed to access this file.", cause) { public constructor(exception: Exception) : this(ThrowableError(exception)) } - public class Unknown( + public class IO( cause: Error? - ) : FilesystemError("An unexpected error occurred on the filesystem.", cause) { + ) : FileSystemError("An unexpected IO error occurred on the filesystem.", cause) { public constructor(exception: Exception) : this(ThrowableError(exception)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt index 36574c6eef..22d649d15e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt @@ -21,29 +21,27 @@ public sealed class ReadError( override val cause: Error? = null ) : Error { - public class Network(public override val cause: HttpError) : - ReadError("A network error occurred.", cause) + public class Access(public override val cause: Error) : + ReadError("An error occurred while attempting to access data.", cause) - public class Filesystem(public override val cause: FilesystemError) : - ReadError("A filesystem error occurred.", cause) + public class Decoding(cause: Error? = null) : + ReadError("An error occurred while attempting to decode the content.", cause) { + + public constructor(message: String) : this(MessageError(message)) + public constructor(exception: Exception) : this(ThrowableError(exception)) + } - /** - * Equivalent to a 507 HTTP error. - * - * Used when the requested range is too large to be read in memory. - */ public class OutOfMemory(override val cause: ThrowableError) : ReadError("The resource is too large to be read on this device.", cause) { public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) } - public class Decoding(cause: Error? = null) : - ReadError("An error occurred while attempting to decode the content.", cause) { + public class UnsupportedOperation(cause: Error? = null) : + ReadError("Could not proceed because an operation was not supported.", cause) { - public constructor(message: String) : this(MessageError(message)) - public constructor(exception: Exception) : this(ThrowableError(exception)) - } + public constructor(message: String) : this(MessageError(message)) + } /** For any other error, such as HTTP 500. */ public class Other(cause: Error) : diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingClosedContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingContainer.kt similarity index 97% rename from readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingClosedContainer.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingContainer.kt index eeaa2de181..c35a23c51d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingClosedContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingContainer.kt @@ -17,7 +17,7 @@ import org.readium.r2.shared.util.Url * * The [routes] will be tested in the given order. */ -public class RoutingClosedContainer( +public class RoutingContainer( private val routes: List> ) : ClosedContainer { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt index c220f6e132..e9b1b129cd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt @@ -42,7 +42,7 @@ public interface DownloadManager { ) : org.readium.r2.shared.util.Error { public class HttpError( - cause: org.readium.r2.shared.util.data.HttpError + cause: org.readium.r2.shared.util.http.HttpError ) : Error(cause.message, cause) public class DeviceNotFound( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index 9a37287dc4..041beba387 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -27,8 +27,8 @@ import org.readium.r2.shared.extensions.tryOr import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.FileBlob -import org.readium.r2.shared.util.data.HttpError -import org.readium.r2.shared.util.data.HttpStatus +import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt index c1408cc52e..08634cd353 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt @@ -8,6 +8,7 @@ package org.readium.r2.shared.util.downloads.foreground import java.io.File import java.io.FileOutputStream +import java.io.IOException import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -17,9 +18,8 @@ import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.tryOrLog -import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.HttpError +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.http.HttpClient @@ -147,7 +147,7 @@ public class ForegroundDownloadManager( Try.success(res.response) } } - } catch (e: Exception) { - Try.failure(HttpError.Other(ThrowableError(e))) + } catch (e: IOException) { + Try.failure(HttpError.IO(e)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index d199b28a1c..69bed530ce 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -9,11 +9,11 @@ package org.readium.r2.shared.util.http import android.os.Bundle import java.io.ByteArrayInputStream import java.io.FileInputStream +import java.io.IOException import java.io.InputStream import java.net.HttpURLConnection import java.net.SocketTimeoutException import java.net.URL -import java.util.concurrent.CancellationException import kotlin.time.Duration import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -24,8 +24,6 @@ import org.readium.r2.shared.util.MessageError 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.data.HttpError -import org.readium.r2.shared.util.data.HttpStatus import org.readium.r2.shared.util.data.InMemoryBlob import org.readium.r2.shared.util.e import org.readium.r2.shared.util.flatMap @@ -209,8 +207,8 @@ public class DefaultHttpClient( ) ) } - } catch (e: Exception) { - Try.failure(wrap(e)) + } catch (e: IOException) { + Try.failure( wrap(e)) } } @@ -336,14 +334,12 @@ public class DefaultHttpClient( /** * Creates an HTTP error from a generic exception. */ -private fun wrap(cause: Throwable): HttpError = +private fun wrap(cause: IOException): HttpError = when (cause) { - is CancellationException -> - throw cause is SocketTimeoutException -> HttpError.Timeout(ThrowableError(cause)) else -> - HttpError.Other(ThrowableError(cause)) + HttpError.IO(cause) } /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt index 85870b6157..ad9fdd2fa1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt @@ -6,19 +6,16 @@ package org.readium.r2.shared.util.http +import java.io.IOException import java.io.InputStream import java.nio.charset.Charset import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject import org.readium.r2.shared.ExperimentalReadiumApi -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.data.HttpError -import org.readium.r2.shared.util.data.HttpStatus import org.readium.r2.shared.util.flatMap -import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.tryRecover public typealias HttpTry = Try @@ -41,76 +38,6 @@ public interface HttpClient { public companion object } -/** - * Represents a successful HTTP response received from a server. - * - * @param request Request associated with the response. - * @param url Final URL of the response. - * @param statusCode Response status code. - * @param headers HTTP response headers, indexed by their name. - * @param mediaType Media type sniffed from the `Content-Type` header and response body. Falls back - * on `application/octet-stream`. - */ -public data class HttpResponse( - val request: HttpRequest, - val url: AbsoluteUrl, - val statusCode: Int, - val headers: Map>, - val mediaType: MediaType -) { - - private val httpHeaders = HttpHeaders(headers) - - /** - * Finds the first value of the first header matching the given name. - * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. - */ - @Deprecated("Use the header method instead.", level = DeprecationLevel.ERROR) - @Suppress("Unused_parameter") - public fun valueForHeader(name: String): String? { - throw NotImplementedError() - } - - /** - * Finds all the values of the first header matching the given name. - * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. - */ - @Deprecated("Use the headers method instead.", level = DeprecationLevel.ERROR) - @Suppress("Unused_parameter") - public fun valuesForHeader(name: String): List { - throw NotImplementedError() - } - - /** - * Finds the last header matching the given name. - * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. - * The returned string can contain a single value or a comma-separated list of values if - * the field supports it. - */ - public fun header(name: String): String? = httpHeaders[name] - - /** - * Finds all the headers matching the given name. - * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. - * Each item of the returned list can contain a single value or a comma-separated list of - * values if the field supports it. - */ - public fun headers(name: String): List = httpHeaders.getAll(name) - - /** - * Indicates whether this server supports byte range requests. - */ - val acceptsByteRanges: Boolean get() = httpHeaders.acceptsByteRanges - - /** - * The expected content length for this response, when known. - * - * Warning: For byte range requests, this will be the length of the chunk, not the full - * resource. - */ - val contentLength: Long? get() = httpHeaders.contentLength -} - /** * HTTP response with streamable content. * @@ -134,9 +61,9 @@ public suspend fun HttpClient.fetch(request: HttpRequest): HttpTry if (from != null && response.response.statusCode != 206 ) { val error = MessageError("Server seems not to support range requests.") - Try.failure(HttpError.Other(error)) + Try.failure(ReadError.UnsupportedOperation(error)) } else { Try.success(response) } } .map { CountingInputStream(it.body) } - .mapFailure { ReadError.Network(it) } .onSuccess { inputStream = it inputStreamStart = from ?: 0 diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt new file mode 100644 index 0000000000..72e8d5a215 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.http + +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.mediatype.MediaType + +/** + * Represents a successful HTTP response received from a server. + * + * @param request Request associated with the response. + * @param url Final URL of the response. + * @param statusCode Response status code. + * @param headers HTTP response headers, indexed by their name. + * @param mediaType Media type sniffed from the `Content-Type` header and response body. Falls back + * on `application/octet-stream`. + */ +public data class HttpResponse( + val request: HttpRequest, + val url: AbsoluteUrl, + val statusCode: Int, + val headers: Map>, + val mediaType: MediaType +) { + + private val httpHeaders = HttpHeaders(headers) + + /** + * Finds the first value of the first header matching the given name. + * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. + */ + @Deprecated("Use the header method instead.", level = DeprecationLevel.ERROR) + @Suppress("Unused_parameter") + public fun valueForHeader(name: String): String? { + throw NotImplementedError() + } + + /** + * Finds all the values of the first header matching the given name. + * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. + */ + @Deprecated("Use the headers method instead.", level = DeprecationLevel.ERROR) + @Suppress("Unused_parameter") + public fun valuesForHeader(name: String): List { + throw NotImplementedError() + } + + /** + * Finds the last header matching the given name. + * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. + * The returned string can contain a single value or a comma-separated list of values if + * the field supports it. + */ + public fun header(name: String): String? = httpHeaders[name] + + /** + * Finds all the headers matching the given name. + * In keeping with the HTTP RFC, HTTP header field names are case-insensitive. + * Each item of the returned list can contain a single value or a comma-separated list of + * values if the field supports it. + */ + public fun headers(name: String): List = httpHeaders.getAll(name) + + /** + * Indicates whether this server supports byte range requests. + */ + val acceptsByteRanges: Boolean get() = httpHeaders.acceptsByteRanges + + /** + * The expected content length for this response, when known. + * + * Warning: For byte range requests, this will be the length of the chunk, not the full + * resource. + */ + val contentLength: Long? get() = httpHeaders.contentLength +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpStatus.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt similarity index 96% rename from readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpStatus.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt index 5293627740..c21dd89e33 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/HttpStatus.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.data +package org.readium.r2.shared.util.http @JvmInline public value class HttpStatus( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index 7db4835724..6cb75947ba 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -16,7 +16,7 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.FileBlob -import org.readium.r2.shared.util.data.FilesystemError +import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.toUrl @@ -52,7 +52,7 @@ public class DirectoryContainer( public companion object { - public suspend operator fun invoke(root: File, mediaTypeRetriever: MediaTypeRetriever): Try { + public suspend operator fun invoke(root: File, mediaTypeRetriever: MediaTypeRetriever): Try { val entries = try { withContext(Dispatchers.IO) { @@ -62,7 +62,7 @@ public class DirectoryContainer( .toSet() } } catch (e: SecurityException) { - return Try.failure(FilesystemError.Forbidden(e)) + return Try.failure(FileSystemError.Forbidden(e)) } val container = DirectoryContainer(root, mediaTypeRetriever, entries) return Try.success(container) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index b438f6ded9..efb71def4d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.readFully import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url @@ -72,7 +73,11 @@ internal class ChannelZipContainer( override suspend fun length(): ResourceTry = entry.size.takeUnless { it == -1L } ?.let { Try.success(it) } - ?: Try.failure(ReadError.Other(UnsupportedOperationException())) + ?: Try.failure( + ReadError.UnsupportedOperation( + MessageError("ZIP entry doesn't provide length for entry $url.") + ) + ) private val compressedLength: Long? get() = diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index e094b32b42..fc8f44e4bd 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -14,7 +14,7 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.data.DecoderError import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.RoutingClosedContainer +import org.readium.r2.shared.util.data.RoutingContainer import org.readium.r2.shared.util.data.readAsRwpm import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpClient @@ -111,7 +111,7 @@ internal class ParserAssetFactory( .toSet() val container = - RoutingClosedContainer( + RoutingContainer( local = SingleResourceContainer( url = Url("manifest.json")!!, asset.resource diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt index 06de2d18a7..9a36c209e8 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt @@ -11,7 +11,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.cover import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.HttpError +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.fetchWithDecoder From 88ffc01b3e6b1cc46086bb6d549bd582a679cd57 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Fri, 17 Nov 2023 12:20:33 +0100 Subject: [PATCH 12/86] Update error handling in testapp --- .../audio/ExoPlayerEngineProvider.kt | 7 +- .../container/ContainerLicenseContainer.kt | 4 +- .../container/ContentZipLicenseContainer.kt | 4 +- .../lcp/license/container/LicenseContainer.kt | 4 +- .../navigator/media2/MediaNavigator.kt | 2 +- .../org/readium/r2/navigator/Navigator.kt | 5 - .../navigator/epub/EpubNavigatorFragment.kt | 2 +- .../navigator/image/ImageNavigatorFragment.kt | 2 +- .../navigator/media/MediaSessionNavigator.kt | 2 +- .../r2/navigator/pdf/PdfNavigatorFragment.kt | 2 +- .../media/audio/AudioEngineProvider.kt | 4 +- .../navigator/media/audio/AudioNavigator.kt | 61 +------ .../media/audio/AudioNavigatorFactory.kt | 86 ++++++++- .../navigator/media/tts/TtsEngineProvider.kt | 4 +- .../navigator/media/tts/TtsNavigator.kt | 89 +-------- .../media/tts/TtsNavigatorFactory.kt | 122 +++++++++++-- .../tts/android/AndroidTtsEngineProvider.kt | 11 +- .../r2/shared/publication/Publication.kt | 4 +- .../protection/ContentProtection.kt | 4 +- .../ContentProtectionSchemeRetriever.kt | 3 +- .../publication/services/CoverService.kt | 4 +- .../r2/shared/util/archive/ArchiveProvider.kt | 6 +- .../util/archive/FileZipArchiveProvider.kt | 6 +- .../r2/shared/util/archive/ZipContainer.kt | 6 +- .../org/readium/r2/shared/util/asset/Asset.kt | 3 +- .../readium/r2/shared/util/data/Container.kt | 39 ++-- .../shared/util/data/ContentProviderError.kt | 6 +- .../r2/shared/util/data/RoutingContainer.kt | 10 +- .../r2/shared/util/http/HttpContainer.kt | 8 +- .../shared/util/mediatype/MediaTypeSniffer.kt | 29 +-- .../util/resource/DirectoryContainer.kt | 9 +- .../shared/util/resource/ResourceContainer.kt | 7 +- .../util/resource/TransformingContainer.kt | 12 +- .../r2/shared/util/zip/ChannelZipContainer.kt | 6 +- .../util/zip/StreamingZipArchiveProvider.kt | 6 +- .../r2/streamer/extensions/Container.kt | 9 +- .../readium/r2/streamer/extensions/Link.kt | 4 +- .../r2/streamer/parser/audio/AudioParser.kt | 4 +- .../r2/streamer/parser/epub/EpubParser.kt | 14 +- .../parser/epub/EpubPositionsService.kt | 4 +- .../r2/streamer/parser/image/ImageParser.kt | 8 +- .../r2/streamer/parser/pdf/PdfParser.kt | 4 +- .../java/org/readium/r2/testapp/Readium.kt | 9 +- .../r2/testapp/bookshelf/BookshelfFragment.kt | 4 +- .../testapp/bookshelf/BookshelfViewModel.kt | 6 +- .../catalogs/CatalogFeedListViewModel.kt | 6 +- .../r2/testapp/catalogs/CatalogViewModel.kt | 21 ++- .../readium/r2/testapp/domain/Bookshelf.kt | 13 +- .../readium/r2/testapp/domain/CoverStorage.kt | 2 +- .../readium/r2/testapp/domain/ImportError.kt | 21 +-- .../r2/testapp/domain/PublicationError.kt | 169 +++++++++++------- .../r2/testapp/domain/PublicationRetriever.kt | 29 +-- .../readium/r2/testapp/reader/OpeningError.kt | 32 ++++ .../r2/testapp/reader/ReaderRepository.kt | 79 ++++---- .../r2/testapp/reader/ReaderViewModel.kt | 11 +- .../r2/testapp/reader/tts/TtsViewModel.kt | 3 +- test-app/src/main/res/values/strings.xml | 16 +- 57 files changed, 561 insertions(+), 486 deletions(-) create mode 100644 test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt diff --git a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerEngineProvider.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerEngineProvider.kt index 601316aa8f..63355d0aa1 100644 --- a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerEngineProvider.kt +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerEngineProvider.kt @@ -18,6 +18,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.indexOfFirstWithHref +import org.readium.r2.shared.util.Try /** * Main component to use the audio navigator with the ExoPlayer adapter. @@ -38,7 +39,7 @@ public class ExoPlayerEngineProvider( publication: Publication, initialLocator: Locator, initialPreferences: ExoPlayerPreferences - ): ExoPlayerEngine { + ): Try { val metadataFactory = metadataProvider.createMetadataFactory(publication) val settingsResolver = ExoPlayerSettingsResolver(defaults) val dataSourceFactory: DataSource.Factory = ExoPlayerDataSource.Factory(publication) @@ -56,7 +57,7 @@ public class ExoPlayerEngineProvider( } ) - return ExoPlayerEngine( + val engine = ExoPlayerEngine( application = application, settingsResolver = settingsResolver, playlist = playlist, @@ -66,6 +67,8 @@ public class ExoPlayerEngineProvider( initialPosition = initialPosition, initialPreferences = initialPreferences ) + + return Try.success(engine) } override fun createPreferenceEditor( diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt index 3d60be65f2..90c7c018ba 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt @@ -9,7 +9,7 @@ package org.readium.r2.lcp.license.container import kotlinx.coroutines.runBlocking import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.resource.Resource @@ -17,7 +17,7 @@ import org.readium.r2.shared.util.resource.Resource * Access to a License Document stored in a read-only container. */ internal class ContainerLicenseContainer( - private val container: ClosedContainer, + private val container: Container, private val entryUrl: Url ) : LicenseContainer { diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt index 6556c32bed..566faae8c6 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt @@ -17,13 +17,13 @@ import java.util.zip.ZipFile import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.toUri internal class ContentZipLicenseContainer( context: Context, - private val container: ClosedContainer, + private val container: Container, private val pathInZip: Url ) : LicenseContainer by ContainerLicenseContainer(container, pathInZip), WritableLicenseContainer { diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt index e500c6feb0..ba14a22409 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt @@ -15,7 +15,7 @@ import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource @@ -72,7 +72,7 @@ internal fun createLicenseContainer( internal fun createLicenseContainer( context: Context, - container: ClosedContainer, + container: Container, mediaType: MediaType ): LicenseContainer { val licensePath = when (mediaType) { diff --git a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/MediaNavigator.kt b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/MediaNavigator.kt index c82815c129..8fa001185d 100644 --- a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/MediaNavigator.kt +++ b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/MediaNavigator.kt @@ -58,7 +58,7 @@ import timber.log.Timber @Deprecated("Use the new AudioNavigator from the readium-navigator-media-audio module.") @OptIn(ExperimentalTime::class) public class MediaNavigator private constructor( - override val publication: Publication, + public val publication: Publication, private val playerFacade: SessionPlayerFacade, private val playerCallback: SessionPlayerCallback, private val configuration: Configuration diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt index fd156b5085..d2e3f892f5 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt @@ -33,11 +33,6 @@ import org.readium.r2.shared.util.data.ReadError */ public interface Navigator { - /** - * Publication rendered by this navigator. - */ - public val publication: Publication - /** * Current position in the publication. * Can be used to save a bookmark to the current position. diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt index 4da3f6df10..e00efbba2e 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt @@ -106,7 +106,7 @@ public typealias JavascriptInterfaceFactory = (resource: Link) -> Any? */ @OptIn(ExperimentalDecorator::class, ExperimentalReadiumApi::class, DelicateReadiumApi::class) public class EpubNavigatorFragment internal constructor( - override val publication: Publication, + private val publication: Publication, private val initialLocator: Locator?, readingOrder: List?, private val initialPreferences: EpubPreferences, diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt index e459e9b757..f25418caa9 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt @@ -52,7 +52,7 @@ import org.readium.r2.shared.publication.services.positions */ @OptIn(ExperimentalReadiumApi::class, DelicateReadiumApi::class) public class ImageNavigatorFragment private constructor( - override val publication: Publication, + private val publication: Publication, private val initialLocator: Locator? = null, internal val listener: Listener? = null ) : Fragment(), OverflowNavigator { diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaSessionNavigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaSessionNavigator.kt index c356b1055e..c150d34e12 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaSessionNavigator.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/MediaSessionNavigator.kt @@ -47,7 +47,7 @@ private val skipBackwardInterval: Duration = 30.seconds @Deprecated("Use the new AudioNavigator from the readium-navigator-media-audio module.") @OptIn(ExperimentalTime::class) public class MediaSessionNavigator( - override val publication: Publication, + public val publication: Publication, public val publicationId: PublicationId, public val controller: MediaControllerCompat, public var listener: Listener? = null diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorFragment.kt index f5d612d661..53d23b3dab 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfNavigatorFragment.kt @@ -54,7 +54,7 @@ import org.readium.r2.shared.util.mediatype.MediaType @ExperimentalReadiumApi @OptIn(DelicateReadiumApi::class) public class PdfNavigatorFragment> internal constructor( - override val publication: Publication, + private val publication: Publication, private val initialLocator: Locator? = null, private val initialPreferences: P, private val listener: Listener?, diff --git a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioEngineProvider.kt b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioEngineProvider.kt index 192faba762..057d35314c 100644 --- a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioEngineProvider.kt +++ b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioEngineProvider.kt @@ -11,6 +11,8 @@ import org.readium.r2.navigator.preferences.PreferencesEditor import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Try /** * To be implemented by adapters for third-party audio engines which can be used with [AudioNavigator]. @@ -23,7 +25,7 @@ public interface AudioEngineProvider? + ): Try, Error> /** * Creates a preferences editor for [publication] and [initialPreferences]. diff --git a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigator.kt b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigator.kt index 6272f0f922..4153d1893f 100644 --- a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigator.kt +++ b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigator.kt @@ -6,10 +6,8 @@ package org.readium.navigator.media.audio -import android.os.Build import androidx.media3.common.Player import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds import kotlin.time.ExperimentalTime import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope @@ -32,8 +30,8 @@ import timber.log.Timber @ExperimentalReadiumApi @OptIn(ExperimentalTime::class, DelicateReadiumApi::class) -public class AudioNavigator> private constructor( - override val publication: Publication, +public class AudioNavigator> internal constructor( + private val publication: Publication, private val audioEngine: AudioEngine, override val readingOrder: ReadingOrder ) : @@ -42,61 +40,6 @@ public class AudioNavigator by audioEngine { - public companion object { - - public suspend operator fun > invoke( - publication: Publication, - audioEngineProvider: AudioEngineProvider, - readingOrder: List = publication.readingOrder, - initialLocator: Locator? = null, - initialPreferences: P? = null - ): AudioNavigator? { - if (readingOrder.isEmpty()) { - return null - } - - val items = readingOrder.map { - ReadingOrder.Item( - href = it.url(), - duration = duration(it, publication) - ) - } - val totalDuration = publication.metadata.duration?.seconds - ?: items.mapNotNull { it.duration } - .takeIf { it.size == items.size } - ?.sum() - - val actualReadingOrder = ReadingOrder(totalDuration, items) - - val actualInitialLocator = - initialLocator?.let { publication.normalizeLocator(it) } - ?: publication.locatorFromLink(publication.readingOrder[0])!! - - val audioEngine = - audioEngineProvider.createEngine( - publication, - actualInitialLocator, - initialPreferences ?: audioEngineProvider.createEmptyPreferences() - ) ?: return null - - return AudioNavigator(publication, audioEngine, actualReadingOrder) - } - - private fun duration(link: Link, publication: Publication): Duration? { - var duration: Duration? = link.duration?.seconds - .takeUnless { it == Duration.ZERO } - - if (duration == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val resource = requireNotNull(publication.get(link)) - val metadataRetriever = MetadataRetriever(resource) - duration = metadataRetriever.duration() - metadataRetriever.close() - } - - return duration - } - } - public data class Location( override val href: Url, override val offset: Duration diff --git a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigatorFactory.kt b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigatorFactory.kt index b6e9277f2d..52356d206b 100644 --- a/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigatorFactory.kt +++ b/readium/navigators/media/audio/src/main/java/org/readium/navigator/media/audio/AudioNavigatorFactory.kt @@ -6,13 +6,24 @@ package org.readium.navigator.media.audio +import android.os.Build +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.ExperimentalTime +import org.readium.r2.navigator.extensions.normalizeLocator +import org.readium.r2.navigator.extensions.sum import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.navigator.preferences.PreferencesEditor +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse @ExperimentalReadiumApi +@OptIn(ExperimentalTime::class, DelicateReadiumApi::class) public class AudioNavigatorFactory, E : PreferencesEditor

> private constructor( private val publication: Publication, @@ -21,8 +32,7 @@ public class AudioNavigatorFactory, + public operator fun , E : PreferencesEditor

> invoke( publication: Publication, audioEngineProvider: AudioEngineProvider @@ -31,6 +41,10 @@ public class AudioNavigatorFactory? { - return AudioNavigator( + initialPreferences: P? = null, + readingOrder: List = publication.readingOrder + ): Try, Error> { + fun duration(link: Link, publication: Publication): Duration? { + var duration: Duration? = link.duration?.seconds + .takeUnless { it == Duration.ZERO } + + if (duration == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val resource = requireNotNull(publication.get(link)) + val metadataRetriever = MetadataRetriever(resource) + duration = metadataRetriever.duration() + metadataRetriever.close() + } + + return duration + } + + val items = readingOrder.map { + AudioNavigator.ReadingOrder.Item( + href = it.url(), + duration = duration(it, publication) + ) + } + val totalDuration = publication.metadata.duration?.seconds + ?: items.mapNotNull { it.duration } + .takeIf { it.size == items.size } + ?.sum() + + val actualReadingOrder = AudioNavigator.ReadingOrder(totalDuration, items) + + val actualInitialLocator = + initialLocator?.let { publication.normalizeLocator(it) } + ?: publication.locatorFromLink(publication.readingOrder[0])!! + + val audioEngine = + audioEngineProvider.createEngine( + publication, + actualInitialLocator, + initialPreferences ?: audioEngineProvider.createEmptyPreferences() + ).getOrElse { + return Try.failure(Error.EngineInitialization(it)) + } + + val audioNavigator = AudioNavigator( publication = publication, - audioEngineProvider = audioEngineProvider, - initialLocator = initialLocator, - initialPreferences = initialPreferences + audioEngine = audioEngine, + readingOrder = actualReadingOrder ) + + return Try.success(audioNavigator) } public fun createAudioPreferencesEditor( diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngineProvider.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngineProvider.kt index da1cb251a5..a8870eed98 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngineProvider.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngineProvider.kt @@ -11,6 +11,8 @@ import androidx.media3.common.PlaybackParameters import org.readium.r2.navigator.preferences.PreferencesEditor import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Try /** * To be implemented by adapters for third-party TTS engines which can be used with [TtsNavigator]. @@ -22,7 +24,7 @@ public interface TtsEngineProvider? + public suspend fun createEngine(publication: Publication, initialPreferences: P): Try, Error> /** * Creates a preferences editor for [publication] and [initialPreferences]. diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt index a2b437ab2e..2c7455a7da 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt @@ -38,9 +38,9 @@ import org.readium.r2.shared.util.tokenizer.TextTokenizer @ExperimentalReadiumApi @OptIn(DelicateReadiumApi::class) public class TtsNavigator, - E : TtsEngine.Error, V : TtsEngine.Voice> private constructor( + E : TtsEngine.Error, V : TtsEngine.Voice> internal constructor( coroutineScope: CoroutineScope, - override val publication: Publication, + private val publication: Publication, private val player: TtsPlayer, private val sessionAdapter: TtsSessionAdapter ) : @@ -49,91 +49,6 @@ public class TtsNavigator, Media3Adapter, Configurable { - public companion object { - - public suspend operator fun , - E : TtsEngine.Error, V : TtsEngine.Voice> invoke( - application: Application, - publication: Publication, - ttsEngineProvider: TtsEngineProvider, - tokenizerFactory: (language: Language?) -> TextTokenizer, - metadataProvider: MediaMetadataProvider, - listener: Listener, - initialLocator: Locator? = null, - initialPreferences: P? = null - ): TtsNavigator? { - if (publication.findService(ContentService::class) == null) { - return null - } - - @Suppress("NAME_SHADOWING") - val initialLocator = - initialLocator?.let { publication.normalizeLocator(it) } - - val actualInitialPreferences = - initialPreferences - ?: ttsEngineProvider.createEmptyPreferences() - - val contentIterator = - TtsUtteranceIterator(publication, tokenizerFactory, initialLocator) - if (!contentIterator.hasNext()) { - return null - } - - val ttsEngine = - ttsEngineProvider.createEngine(publication, actualInitialPreferences) - ?: return null - - val metadataFactory = - metadataProvider.createMetadataFactory(publication) - - val playlistMetadata = - metadataFactory.publicationMetadata() - - val mediaItems = - publication.readingOrder.indices.map { index -> - val metadata = metadataFactory.resourceMetadata(index) - MediaItem.Builder() - .setMediaMetadata(metadata) - .build() - } - - val ttsPlayer = - TtsPlayer(ttsEngine, contentIterator, actualInitialPreferences) - ?: return null - - val coroutineScope = - MainScope() - - val playbackParameters = - ttsPlayer.settings.mapStateIn(coroutineScope) { - ttsEngineProvider.getPlaybackParameters(it) - } - - val onSetPlaybackParameters = { parameters: PlaybackParameters -> - val newPreferences = ttsEngineProvider.updatePlaybackParameters( - ttsPlayer.lastPreferences, - parameters - ) - ttsPlayer.submitPreferences(newPreferences) - } - - val sessionAdapter = - TtsSessionAdapter( - application, - ttsPlayer, - playlistMetadata, - mediaItems, - listener::onStopRequested, - playbackParameters, - onSetPlaybackParameters, - ttsEngineProvider::mapEngineError - ) - - return TtsNavigator(coroutineScope, publication, ttsPlayer, sessionAdapter) - } - } - public interface Listener { public fun onStopRequested() diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt index 53ed0398ec..4e97d3d2d0 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt @@ -7,23 +7,35 @@ package org.readium.navigator.media.tts import android.app.Application +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackParameters +import kotlinx.coroutines.MainScope import org.readium.navigator.media.common.DefaultMediaMetadataProvider import org.readium.navigator.media.common.MediaMetadataProvider import org.readium.navigator.media.tts.android.AndroidTtsDefaults import org.readium.navigator.media.tts.android.AndroidTtsEngine import org.readium.navigator.media.tts.android.AndroidTtsEngineProvider +import org.readium.navigator.media.tts.session.TtsSessionAdapter +import org.readium.r2.navigator.extensions.normalizeLocator import org.readium.r2.navigator.preferences.PreferencesEditor +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.extensions.mapStateIn import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.content.Content +import org.readium.r2.shared.publication.services.content.ContentService import org.readium.r2.shared.publication.services.content.content import org.readium.r2.shared.util.Language +import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.tokenizer.DefaultTextContentTokenizer import org.readium.r2.shared.util.tokenizer.TextTokenizer import org.readium.r2.shared.util.tokenizer.TextUnit @ExperimentalReadiumApi +@OptIn(DelicateReadiumApi::class) public class TtsNavigatorFactory, E : PreferencesEditor

, F : TtsEngine.Error, V : TtsEngine.Voice> private constructor( private val application: Application, @@ -92,6 +104,20 @@ public class TtsNavigatorFactory? { - return TtsNavigator( - application, - publication, - ttsEngineProvider, - tokenizerFactory, - metadataProvider, - listener, - initialLocator, + ): Try, Error> { + if (publication.findService(ContentService::class) == null) { + return Try.failure( + Error.UnsupportedPublication( + MessageError("No content service found in publication.") + ) + ) + } + + @Suppress("NAME_SHADOWING") + val initialLocator = + initialLocator?.let { publication.normalizeLocator(it) } + + val actualInitialPreferences = initialPreferences - ) + ?: ttsEngineProvider.createEmptyPreferences() + + val contentIterator = + TtsUtteranceIterator(publication, tokenizerFactory, initialLocator) + if (!contentIterator.hasNext()) { + return Try.failure( + Error.UnsupportedPublication( + MessageError("Content iterator is empty.") + ) + ) + } + + val ttsEngine = + ttsEngineProvider.createEngine(publication, actualInitialPreferences) + .getOrElse { + return Try.failure( + Error.EngineInitialization() + ) + } + + val metadataFactory = + metadataProvider.createMetadataFactory(publication) + + val playlistMetadata = + metadataFactory.publicationMetadata() + + val mediaItems = + publication.readingOrder.indices.map { index -> + val metadata = metadataFactory.resourceMetadata(index) + MediaItem.Builder() + .setMediaMetadata(metadata) + .build() + } + + val ttsPlayer = + TtsPlayer(ttsEngine, contentIterator, actualInitialPreferences) + ?: return Try.failure( + Error.UnsupportedPublication(MessageError("Empty content.")) + ) + + val coroutineScope = + MainScope() + + val playbackParameters = + ttsPlayer.settings.mapStateIn(coroutineScope) { + ttsEngineProvider.getPlaybackParameters(it) + } + + val onSetPlaybackParameters = { parameters: PlaybackParameters -> + val newPreferences = ttsEngineProvider.updatePlaybackParameters( + ttsPlayer.lastPreferences, + parameters + ) + ttsPlayer.submitPreferences(newPreferences) + } + + val sessionAdapter = + TtsSessionAdapter( + application, + ttsPlayer, + playlistMetadata, + mediaItems, + listener::onStopRequested, + playbackParameters, + onSetPlaybackParameters, + ttsEngineProvider::mapEngineError + ) + + val ttsNavigator = + TtsNavigator(coroutineScope, publication, ttsPlayer, sessionAdapter) + + return Try.success(ttsNavigator) } public fun createPreferencesEditor(preferences: P): E = diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngineProvider.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngineProvider.kt index ce0a9c4d17..b638a3138f 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngineProvider.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngineProvider.kt @@ -13,6 +13,9 @@ import androidx.media3.common.PlaybackParameters import org.readium.navigator.media.tts.TtsEngineProvider import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.Try @ExperimentalReadiumApi @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @@ -26,16 +29,20 @@ public class AndroidTtsEngineProvider( override suspend fun createEngine( publication: Publication, initialPreferences: AndroidTtsPreferences - ): AndroidTtsEngine? { + ): Try { val settingsResolver = AndroidTtsSettingsResolver(publication.metadata, defaults) - return AndroidTtsEngine( + val engine = AndroidTtsEngine( context, settingsResolver, voiceSelector, initialPreferences + ) ?: return Try.failure( + MessageError("Initialization of Android Tts service failed.") ) + + return Try.success(engine) } override fun createPreferencesEditor( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index 26a687ad61..d6806a2009 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -32,7 +32,7 @@ import org.readium.r2.shared.publication.services.search.SearchService import org.readium.r2.shared.util.Closeable import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.EmptyContainer import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpClient @@ -54,7 +54,7 @@ internal typealias ServiceFactory = (Publication.Service.Context) -> Publication */ public typealias PublicationId = String -public typealias PublicationContainer = ClosedContainer +public typealias PublicationContainer = Container /** * The Publication shared model is the entry-point for all the metadata and services diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index ca8d86056e..04a6bcc106 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -25,7 +25,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.ContentProtectionService import org.readium.r2.shared.util.Error as BaseError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource @@ -85,7 +85,7 @@ public interface ContentProtection { */ public data class Asset( val mediaType: MediaType, - val container: ClosedContainer, + val container: Container, val onCreatePublication: Publication.Builder.() -> Unit = {} ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt index 274e1ed53a..501a84a3ff 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt @@ -11,6 +11,7 @@ import kotlin.let import kotlin.takeIf import org.readium.r2.shared.util.Error as BaseError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse /** @@ -33,7 +34,7 @@ public class ContentProtectionSchemeRetriever( public object NoContentProtectionFound : Error("No content protection recognized the given asset.", null) - public class AccessError(cause: org.readium.r2.shared.util.Error?) : + public class AccessError(override val cause: ReadError) : Error("An error occurred while trying to read asset.", cause) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index bcbdd8cc57..607c9072e5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -21,7 +21,7 @@ import org.readium.r2.shared.publication.firstWithRel 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.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.shared.util.data.readAsBitmap @@ -127,7 +127,7 @@ internal class ExternalCoverService( internal class ResourceCoverService( private val coverUrl: Url, - private val container: ClosedContainer + private val container: Container ) : CoverService { override suspend fun cover(): Bitmap? { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt index 667f589be0..6cf61803ae 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt @@ -9,7 +9,7 @@ package org.readium.r2.shared.util.archive import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaTypeSniffer @@ -51,7 +51,7 @@ public interface ArchiveFactory { public suspend fun create( resource: Blob, password: String? = null - ): Try, Error> + ): Try, Error> } public class CompositeArchiveFactory( @@ -63,7 +63,7 @@ public class CompositeArchiveFactory( override suspend fun create( resource: Blob, password: String? - ): Try, ArchiveFactory.Error> { + ): Try, ArchiveFactory.Error> { for (factory in factories) { factory.create(resource, password) .getOrElse { error -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt index 5f2dc05e2b..547519d1f3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse @@ -72,7 +72,7 @@ public class FileZipArchiveProvider( override suspend fun create( resource: Blob, password: String? - ): Try, ArchiveFactory.Error> { + ): Try, ArchiveFactory.Error> { if (password != null) { return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) } @@ -91,7 +91,7 @@ public class FileZipArchiveProvider( } // Internal for testing purpose - internal suspend fun open(file: File): Try, ArchiveFactory.Error> = + internal suspend fun open(file: File): Try, ArchiveFactory.Error> = withContext(Dispatchers.IO) { try { val archive = JavaZipContainer(ZipFile(file), file, mediaTypeRetriever) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt index 5c072aa920..20320b92ba 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt @@ -20,7 +20,7 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse @@ -37,7 +37,7 @@ internal class JavaZipContainer( private val archive: ZipFile, file: File, private val mediaTypeRetriever: MediaTypeRetriever -) : ClosedContainer { +) : Container { private inner class Entry(private val url: Url, private val entry: ZipEntry) : Resource { @@ -145,7 +145,7 @@ internal class JavaZipContainer( override val source: AbsoluteUrl = file.toUrl() - override suspend fun entries(): Set = + override val entries: Set = tryOrLog { archive.entries().toList() } .orEmpty() .filterNot { it.isDirectory } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt index 0c51e494dd..128da80298 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt @@ -6,7 +6,6 @@ package org.readium.r2.shared.util.asset -import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.mediatype.MediaType /** @@ -50,7 +49,7 @@ public sealed class Asset { public class Container( override val mediaType: MediaType, public val containerType: MediaType, - public val container: ClosedContainer + public val container: org.readium.r2.shared.util.data.Container ) : Asset() { override suspend fun close() { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index 3dd45c0e44..75573e3ff7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -8,57 +8,44 @@ package org.readium.r2.shared.util.data import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.SuspendingCloseable -import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.resource.Resource /** * A container provides access to a list of [Resource] entries. */ -public interface Container : SuspendingCloseable { +public interface Container : Iterable, SuspendingCloseable { /** * Direct source to this container, when available. */ public val source: AbsoluteUrl? get() = null + /** + * List of all the container entries. + */ + public val entries: Set + + override fun iterator(): Iterator = + entries.iterator() + /** * Returns the [Entry] at the given [url]. * * A [Entry] is always returned, since for some cases we can't know if it exists before actually * fetching it, such as HTTP. Therefore, errors are handled at the Entry level. */ - public fun get(url: Url): E? -} + public operator fun get(url: Url): E? -public interface ClosedContainer : Container { - - /** - * List of all the container entries. - */ - public suspend fun entries(): Set } /** A [Container] providing no resources at all. */ -public class EmptyContainer : ClosedContainer { +public class EmptyContainer : + Container { - override suspend fun entries(): Set = emptySet() + override val entries: Set = emptySet() override fun get(url: Url): E? = null override suspend fun close() {} } - -/** - * Returns whether an entry exists in the container. - */ -internal suspend fun Container.contains(url: Url): Try { - if (this is ClosedContainer) { - return Try.success(url in entries()) - } - - return get(url) - ?.read(range = 0L..1L) - ?.map { true } - ?: Try.success(false) -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt index 2e5430f50a..d1fa05f965 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt @@ -16,21 +16,21 @@ public sealed class ContentProviderError( public class FileNotFound( cause: Error? - ) : FileSystemError("File not found.", cause) { + ) : ContentProviderError("File not found.", cause) { public constructor(exception: Exception) : this(ThrowableError(exception)) } public class NotAvailable( cause: Error? - ) : FileSystemError("Content Provider recently crashed.", cause) { + ) : ContentProviderError("Content Provider recently crashed.", cause) { public constructor(exception: Exception) : this(ThrowableError(exception)) } public class IO( cause: Error? - ) : FileSystemError("An IO error occurred.", cause) { + ) : ContentProviderError("An IO error occurred.", cause) { public constructor(exception: Exception) : this(ThrowableError(exception)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingContainer.kt index c35a23c51d..f58eed51a3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingContainer.kt @@ -19,7 +19,7 @@ import org.readium.r2.shared.util.Url */ public class RoutingContainer( private val routes: List> -) : ClosedContainer { +) : Container { /** * Holds a child fetcher and the predicate used to determine if it can answer a request. @@ -27,11 +27,11 @@ public class RoutingContainer( * The default value for [accepts] means that the fetcher will accept any link. */ public class Route( - public val container: ClosedContainer, + public val container: Container, public val accepts: (Url) -> Boolean = { true } ) - public constructor(local: ClosedContainer, remote: ClosedContainer) : + public constructor(local: Container, remote: Container) : this( listOf( Route(local, accepts = ::isLocal), @@ -39,8 +39,8 @@ public class RoutingContainer( ) ) - override suspend fun entries(): Set = - routes.fold(emptySet()) { acc, route -> acc + route.container.entries() } + override val entries: Set = + routes.fold(emptySet()) { acc, route -> acc + route.container.entries } override fun get(url: Url): E? = routes.firstOrNull { it.accepts(url) }?.container?.get(url) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt index 337e134238..fce7648d1c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt @@ -8,7 +8,7 @@ package org.readium.r2.shared.util.http import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.resource.Resource /** @@ -23,10 +23,8 @@ import org.readium.r2.shared.util.resource.Resource public class HttpContainer( private val client: HttpClient, private val baseUrl: Url? = null, - private val entries: Set -) : ClosedContainer { - - override suspend fun entries(): Set = entries + override val entries: Set +) : Container { override fun get(url: Url): Resource? { val absoluteUrl = (baseUrl?.resolve(url) ?: url) as? AbsoluteUrl diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index c5cf03e422..0e90127c2e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -23,11 +23,9 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.BlobInputStream -import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.DecoderError import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.contains import org.readium.r2.shared.util.data.containsJsonKeys import org.readium.r2.shared.util.data.readAsJson import org.readium.r2.shared.util.data.readAsRwpm @@ -512,7 +510,7 @@ public class WebPubMediaTypeSniffer : MediaTypeSniffer { override suspend fun sniffContainer(container: Container<*>): Try { // Reads a RWPM from a manifest.json archive entry. val manifest: Manifest = - container.get(RelativeUrl("manifest.json")!!) + container[RelativeUrl("manifest.json")!!] ?.read() ?.getOrElse { error -> return Try.failure(MediaTypeSnifferError.DataAccess(error)) @@ -520,10 +518,7 @@ public class WebPubMediaTypeSniffer : MediaTypeSniffer { ?.let { tryOrNull { Manifest.fromJSON(JSONObject(String(it))) } } ?: return Try.failure(MediaTypeSnifferError.NotRecognized) - val isLcpProtected = container.contains(RelativeUrl("license.lcpl")!!) - .getOrElse { - return Try.failure(MediaTypeSnifferError.DataAccess(it)) - } + val isLcpProtected = RelativeUrl("license.lcpl")!! in container if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { return if (isLcpProtected) { @@ -589,8 +584,7 @@ public class EpubMediaTypeSniffer : MediaTypeSniffer { } override suspend fun sniffContainer(container: Container<*>): Try { - val mimetype = container - .get(RelativeUrl("mimetype")!!) + val mimetype = container[RelativeUrl("mimetype")!!] ?.read() ?.getOrElse { error -> return Try.failure(MediaTypeSnifferError.DataAccess(error)) @@ -624,13 +618,12 @@ public object LpfMediaTypeSniffer : MediaTypeSniffer { } override suspend fun sniffContainer(container: Container<*>): Try { - container.contains(RelativeUrl("index.html")!!) - .getOrElse { return Try.failure(MediaTypeSnifferError.DataAccess(it)) } - .takeIf { it } - ?.let { return Try.success(MediaType.LPF) } + if (RelativeUrl("index.html")!! in container) { + return Try.success(MediaType.LPF) + } // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. - container.get(RelativeUrl("publication.json")!!) + container[RelativeUrl("publication.json")!!] ?.read() ?.getOrElse { error -> return Try.failure(MediaTypeSnifferError.DataAccess(error)) @@ -718,15 +711,11 @@ public object ArchiveMediaTypeSniffer : MediaTypeSniffer { } override suspend fun sniffContainer(container: Container<*>): Try { - if (container !is ClosedContainer<*>) { - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - fun isIgnored(url: Url): Boolean = url.filename?.startsWith(".") == true || url.filename == "Thumbs.db" - suspend fun archiveContainsOnlyExtensions(fileExtensions: List): Boolean = - container.entries().all { url -> + fun archiveContainsOnlyExtensions(fileExtensions: List): Boolean = + container.all { url -> isIgnored(url) || url.extension?.let { fileExtensions.contains( it.lowercase(Locale.ROOT) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index 6cb75947ba..660d9ea748 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -13,7 +13,6 @@ import org.readium.r2.shared.extensions.isParentOf import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.data.FileSystemError @@ -27,8 +26,8 @@ import org.readium.r2.shared.util.toUrl public class DirectoryContainer( private val root: File, private val mediaTypeRetriever: MediaTypeRetriever, - private val entries: Set -) : ClosedContainer { + override val entries: Set +) : Container { private fun File.toResource(): Resource { return GuessMediaTypeResourceAdapter( @@ -38,10 +37,6 @@ public class DirectoryContainer( ) } - override suspend fun entries(): Set { - return entries - } - override fun get(url: Url): Resource? = (url as? RelativeUrl)?.path ?.let { File(root, it) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt index d73205ec30..1001a90fea 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt @@ -8,7 +8,6 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ClosedContainer import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError @@ -18,9 +17,9 @@ public typealias ResourceContainer = Container /** A [Container] for a single [Resource]. */ public class SingleResourceContainer( - private val url: Url, + url: Url, private val resource: Resource -) : ClosedContainer { +) : Container { private class Entry( private val resource: Resource @@ -31,7 +30,7 @@ public class SingleResourceContainer( } } - override suspend fun entries(): Set = setOf(url) + override val entries: Set = setOf(url) override fun get(url: Url): Resource? { if (url.removeFragment().removeQuery() != url) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt index 214e4dfb4d..ab17304603 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt @@ -7,7 +7,7 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container /** * Implements the transformation of a Resource. It can be used, for example, to decrypt, @@ -23,15 +23,15 @@ public typealias ResourceTransformer = (Resource) -> Resource * functions. */ public class TransformingContainer( - private val container: ClosedContainer, + private val container: Container, private val transformers: List<(Url, Resource) -> Resource> -) : ClosedContainer { +) : Container { - public constructor(container: ClosedContainer, transformer: (Url, Resource) -> Resource) : + public constructor(container: Container, transformer: (Url, Resource) -> Resource) : this(container, listOf(transformer)) - override suspend fun entries(): Set = - container.entries() + override val entries: Set = + container.entries override fun get(url: Url): Resource? { val originalResource = container.get(url) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index efb71def4d..c7289459fc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -17,7 +17,7 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.archive.ArchiveProperties import org.readium.r2.shared.util.archive.archive -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.data.unwrapReadException @@ -37,7 +37,7 @@ internal class ChannelZipContainer( private val zipFile: ZipFile, override val source: AbsoluteUrl?, private val mediaTypeRetriever: MediaTypeRetriever -) : ClosedContainer { +) : Container { private inner class Entry( private val url: Url, @@ -156,7 +156,7 @@ internal class ChannelZipContainer( } } - override suspend fun entries(): Set = + override val entries: Set = zipFile.entries.toList() .filterNot { it.isDirectory } .mapNotNull { entry -> Url.fromDecodedPath(entry.name) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 0d4a705d31..3df1955525 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.archive.ArchiveProvider import org.readium.r2.shared.util.data.Blob -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.data.unwrapReadException @@ -64,7 +64,7 @@ public class StreamingZipArchiveProvider( override suspend fun create( resource: Blob, password: String? - ): Try, ArchiveFactory.Error> { + ): Try, ArchiveFactory.Error> { if (password != null) { return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) } @@ -90,7 +90,7 @@ public class StreamingZipArchiveProvider( blob: Blob, wrapError: (ReadError) -> IOException, sourceUrl: AbsoluteUrl? - ): ClosedContainer = withContext(Dispatchers.IO) { + ): Container = withContext(Dispatchers.IO) { val datasourceChannel = BlobChannel(blob, wrapError) val channel = wrapBaseChannel(datasourceChannel) val zipFile = ZipFile(channel, true) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt index a52848b2bf..474213b662 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt @@ -11,21 +11,20 @@ package org.readium.r2.streamer.extensions import java.io.File import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.readAsXml import org.readium.r2.shared.util.use import org.readium.r2.shared.util.xml.ElementNode /** Returns the resource data as an XML Document at the given [path], or null. */ -internal suspend fun ClosedContainer<*>.readAsXmlOrNull(path: String): ElementNode? = +internal suspend fun Container<*>.readAsXmlOrNull(path: String): ElementNode? = Url.fromDecodedPath(path)?.let { readAsXmlOrNull(it) } /** Returns the resource data as an XML Document at the given [url], or null. */ -internal suspend fun ClosedContainer<*>.readAsXmlOrNull(url: Url): ElementNode? = +internal suspend fun Container<*>.readAsXmlOrNull(url: Url): ElementNode? = get(url)?.use { it.readAsXml().getOrNull() } -internal suspend fun ClosedContainer<*>.guessTitle(): String? { - val entries = entries() +internal fun Container<*>.guessTitle(): String? { val firstEntry = entries.firstOrNull() ?: return null val commonFirstComponent = entries.pathCommonFirstComponent() ?: return null diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt index 702f237d65..75c76ae890 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt @@ -8,12 +8,12 @@ package org.readium.r2.streamer.extensions import org.readium.r2.shared.publication.Link import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use -internal suspend fun ClosedContainer.linkForUrl( +internal suspend fun Container.linkForUrl( url: Url, mediaType: MediaType? = null ): Link = diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index 51af198781..5ce870ed6c 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -39,13 +39,13 @@ public class AudioParser : PublicationParser { val readingOrder = if (asset.mediaType.matches(MediaType.ZAB)) { - asset.container.entries() + asset.container .filter { zabCanContain(it) } .sortedBy { it.toString() } .toMutableList() } else { listOfNotNull( - asset.container.entries().firstOrNull() + asset.container.entries.firstOrNull() ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 0602ce7fa7..1e8ca205da 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -17,7 +17,7 @@ import org.readium.r2.shared.publication.services.search.StringSearchService import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.DecoderError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.readAsXml @@ -109,7 +109,7 @@ public class EpubParser( return Try.success(builder) } - private suspend fun getRootFilePath(container: ClosedContainer): Try { + private suspend fun getRootFilePath(container: Container): Try { val containerXmlUrl =Url("META-INF/container.xml")!! val containerXmlResource = container @@ -135,14 +135,14 @@ public class EpubParser( ) } - private suspend fun parseEncryptionData(container: ClosedContainer): Map = + private suspend fun parseEncryptionData(container: Container): Map = container.readAsXmlOrNull("META-INF/encryption.xml") ?.let { EncryptionParser.parse(it) } ?: emptyMap() private suspend fun parseNavigationData( packageDocument: PackageDocument, - container: ClosedContainer + container: Container ): Map> = parseNavigationDocument(packageDocument, container) ?: parseNcx(packageDocument, container) @@ -150,7 +150,7 @@ public class EpubParser( private suspend fun parseNavigationDocument( packageDocument: PackageDocument, - container: ClosedContainer + container: Container ): Map>? = packageDocument.manifest .firstOrNull { it.properties.contains(Vocabularies.ITEM + "nav") } @@ -162,7 +162,7 @@ public class EpubParser( private suspend fun parseNcx( packageDocument: PackageDocument, - container: ClosedContainer + container: Container ): Map>? { val ncxItem = if (packageDocument.spine.toc != null) { @@ -178,7 +178,7 @@ public class EpubParser( ?.takeUnless { it.isEmpty() } } - private suspend fun parseDisplayOptions(container: ClosedContainer): Map { + private suspend fun parseDisplayOptions(container: Container): Map { val displayOptionsXml = container.readAsXmlOrNull("META-INF/com.apple.ibooks.display-options.xml") ?: container.readAsXmlOrNull("META-INF/com.kobobooks.display-options.xml") diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt index bb9fde83c7..3fb4c54b8a 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt @@ -18,7 +18,7 @@ import org.readium.r2.shared.publication.presentation.presentation import org.readium.r2.shared.publication.services.PositionsService import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.archive.archive -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use @@ -35,7 +35,7 @@ import org.readium.r2.shared.util.use public class EpubPositionsService( private val readingOrder: List, private val presentation: Presentation, - private val container: ClosedContainer, + private val container: Container, private val reflowableStrategy: ReflowableStrategy ) : PositionsService { diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index 32466b633d..c9eeace3d3 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -14,7 +14,7 @@ import org.readium.r2.shared.publication.services.PerResourcePositionsService import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ClosedContainer +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType @@ -43,11 +43,11 @@ public class ImageParser : PublicationParser { val readingOrder = if (asset.mediaType.matches(MediaType.CBZ)) { - (asset.container.entries()) + (asset.container) .filter { !it.isHiddenOrThumbs && entryIsBitmap(asset.container, it) } .sortedBy { it.toString() } } else { - listOfNotNull(asset.container.entries().firstOrNull()) + listOfNotNull(asset.container.firstOrNull()) } .map { asset.container.linkForUrl(it) } .toMutableList() @@ -86,6 +86,6 @@ public class ImageParser : PublicationParser { return Try.success(publicationBuilder) } - private suspend fun entryIsBitmap(container: ClosedContainer, url: Url) = + private suspend fun entryIsBitmap(container: Container, url: Url) = container.get(url)!!.use { it.mediaType() }.getOrNull()?.isBitmap == true } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index 632fc95174..32de55825f 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -43,11 +43,11 @@ public class PdfParser( return Try.failure(PublicationParser.Error.UnsupportedFormat()) } - val url = asset.container.entries() + val url = asset.container.entries .firstOrNull() val resource = url - ?.let { asset.container.get(it) } + ?.let { asset.container[it] } ?: return Try.failure( PublicationParser.Error.ReadError( ReadError.Decoding( diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 873849eba7..3a5437df95 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -19,12 +19,12 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.asset.CompositeResourceFactory import org.readium.r2.shared.util.asset.HttpResourceFactory -import org.readium.r2.shared.util.data.FileResourceFactory +import org.readium.r2.shared.util.asset.FileResourceFactory import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaTypeRetriever -import org.readium.r2.shared.util.resource.ContentResourceFactory +import org.readium.r2.shared.util.asset.ContentResourceFactory import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider import org.readium.r2.streamer.PublicationFactory @@ -52,9 +52,7 @@ class Readium(context: Context) { ) val assetRetriever = AssetRetriever( - mediaTypeRetriever, resourceFactory, - context.contentResolver, archiveProviders ) @@ -84,8 +82,7 @@ class Readium(context: Context) { ) val protectionRetriever = ContentProtectionSchemeRetriever( - contentProtections, - mediaTypeRetriever + contentProtections ) /** diff --git a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt index 14623f01b5..08a913a9e3 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt @@ -22,7 +22,7 @@ import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar -import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.testapp.Application import org.readium.r2.testapp.R import org.readium.r2.testapp.data.model.Book @@ -140,7 +140,7 @@ class BookshelfFragment : Fragment() { dialog.cancel() } .setPositiveButton(getString(R.string.ok)) { _, _ -> - val url = Url(urlEditText.text.toString()) + val url = AbsoluteUrl(urlEditText.text.toString()) if (url == null || !URLUtil.isValidUrl(urlEditText.text.toString())) { urlEditText.error = getString(R.string.invalid_url) return@setPositiveButton diff --git a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfViewModel.kt index 745b5c6637..a98821f0aa 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfViewModel.kt @@ -12,7 +12,7 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch -import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.toUrl import org.readium.r2.testapp.data.model.Book import org.readium.r2.testapp.reader.ReaderActivityContract @@ -36,10 +36,10 @@ class BookshelfViewModel(application: Application) : AndroidViewModel(applicatio } fun addPublicationFromStorage(uri: Uri) { - app.bookshelf.addPublicationFromStorage(uri.toUrl()!!) + app.bookshelf.addPublicationFromStorage(uri.toUrl()!! as AbsoluteUrl) } - fun addPublicationFromWeb(url: Url) { + fun addPublicationFromWeb(url: AbsoluteUrl) { app.bookshelf.addPublicationFromWeb(url) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt index 2610e322f9..1dec3fa22f 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt @@ -15,10 +15,10 @@ import org.json.JSONObject import org.readium.r2.opds.OPDS1Parser import org.readium.r2.opds.OPDS2Parser import org.readium.r2.shared.opds.ParseData +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.fetchWithDecoder import org.readium.r2.testapp.data.CatalogRepository @@ -59,10 +59,10 @@ class CatalogFeedListViewModel(application: Application) : AndroidViewModel(appl } private suspend fun parseURL(urlString: String): Try { - val url = Url(urlString) + val url = AbsoluteUrl(urlString) ?: return Try.failure(MessageError("Invalid URL")) - return httpClient.fetchWithDecoder(HttpRequest(url.toString())) { + return httpClient.fetchWithDecoder(HttpRequest(url)) { val result = it.body if (isJson(result)) { OPDS2Parser.parse(result, url) diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogViewModel.kt index 3d9c271563..954642a6cd 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogViewModel.kt @@ -9,13 +9,13 @@ package org.readium.r2.testapp.catalogs import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope -import java.net.MalformedURLException import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import org.readium.r2.opds.OPDS1Parser import org.readium.r2.opds.OPDS2Parser import org.readium.r2.shared.opds.ParseData import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.testapp.data.model.Catalog @@ -31,17 +31,16 @@ class CatalogViewModel(application: Application) : AndroidViewModel(application) fun parseCatalog(catalog: Catalog) = viewModelScope.launch { var parseRequest: Try? = null - catalog.href.let { - val request = HttpRequest(it) - try { - parseRequest = if (catalog.type == 1) { - OPDS1Parser.parseRequest(request, app.readium.httpClient) - } else { - OPDS2Parser.parseRequest(request, app.readium.httpClient) + catalog.href.let {href -> + AbsoluteUrl(href) + ?.let { HttpRequest(it) } + ?.let { request -> + parseRequest = if (catalog.type == 1) { + OPDS1Parser.parseRequest(request, app.readium.httpClient) + } else { + OPDS2Parser.parseRequest(request, app.readium.httpClient) + } } - } catch (e: MalformedURLException) { - channel.send(Event.CatalogParseFailed) - } } parseRequest?.onSuccess { channel.send(Event.CatalogParseSuccess(it)) diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index 3739acfb67..03a379fd1f 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -18,7 +18,7 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.toUrl import org.readium.r2.streamer.PublicationFactory @@ -127,12 +127,17 @@ class Bookshelf( assetRetriever.retrieve(url) .getOrElse { return Try.failure( - ImportError.PublicationError(PublicationError.UnsupportedAsset()) + ImportError.PublicationError(PublicationError(it)) ) } val drmScheme = protectionRetriever.retrieve(asset) + .getOrElse { + return Try.failure( + ImportError.PublicationError(PublicationError(it)) + ) + } publicationFactory.open( asset, @@ -143,7 +148,9 @@ class Bookshelf( coverStorage.storeCover(publication, coverUrl) .getOrElse { return Try.failure( - ImportError.ResourceError(ReadError.Filesystem(it)) + ImportError.PublicationError( + PublicationError.FsUnexpected(FileSystemError.IO(it)) + ) ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt index 9a36c209e8..3b9378aa86 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt @@ -40,7 +40,7 @@ class CoverStorage( tryOrLog { when { isFile -> toFile()?.toBitmap() - isHttp -> httpClient.fetchBitmap(HttpRequest(toString())).getOrNull() + isHttp -> httpClient.fetchBitmap(HttpRequest(this)).getOrNull() else -> null } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt index 504d50ae22..ad5363ea02 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt @@ -8,7 +8,6 @@ package org.readium.r2.testapp.domain import androidx.annotation.StringRes import org.readium.r2.shared.UserException -import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.testapp.R @@ -28,24 +27,8 @@ sealed class ImportError( ) : ImportError(cause) class PublicationError( - override val cause: UserException - ) : ImportError(cause) { - - companion object { - - operator fun invoke( - error: AssetError - ): ImportError = PublicationError( - org.readium.r2.testapp.domain.PublicationError( - error - ) - ) - } - } - - class ResourceError( - val error: ReadError - ) : ImportError(R.string.import_publication_unexpected_io_exception) + override val cause: org.readium.r2.testapp.domain.PublicationError + ) : ImportError(cause) class DownloadFailed( val error: DownloadManager.Error diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index 1d0ef58018..1a151ae04f 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -8,88 +8,137 @@ package org.readium.r2.testapp.domain import androidx.annotation.StringRes import org.readium.r2.shared.UserException +import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetriever import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.data.ContentProviderError +import org.readium.r2.shared.util.data.FileSystemError +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.http.HttpStatus +import org.readium.r2.streamer.PublicationFactory import org.readium.r2.testapp.R sealed class PublicationError(@StringRes userMessageId: Int) : UserException(userMessageId) { - class Network(val error: Error) : PublicationError( - R.string.publication_error_network_unexpected - ) + class HttpNotFound(val error: Error) : + PublicationError(R.string.publication_error_network_not_found) - class Filesystem(val error: Error) : PublicationError(R.string.publication_error_filesystem) + class HttpForbidden(val error: Error) : + PublicationError(R.string.publication_error_network_forbidden) - class NotFound(val error: Error) : PublicationError(R.string.publication_error_not_found) + class HttpConnectivity(val error: Error) : + PublicationError(R.string.publication_error_network_connectivity) - class OutOfMemory(val error: Error) : PublicationError(R.string.publication_error_out_of_memory) + class HttpUnexpected(val error: Error) : + PublicationError(R.string.publication_error_network_unexpected) - class SchemeNotSupported(val error: Error) : PublicationError( - R.string.publication_error_scheme_not_supported - ) + class FsNotFound(val error: Error) : + PublicationError(R.string.publication_error_filesystem_not_found) - class UnsupportedAsset(val error: Error? = null) : PublicationError( - R.string.publication_error_unsupported_asset - ) + class FsUnexpected(val error: Error) : + PublicationError(R.string.publication_error_filesystem_unexpected) - class InvalidPublication(val error: Error) : PublicationError( - R.string.publication_error_invalid_publication - ) + class OutOfMemory(val error: Error) : + PublicationError(R.string.publication_error_out_of_memory) - class IncorrectCredentials(val error: Error) : PublicationError( - R.string.publication_error_incorrect_credentials - ) + class UnsupportedScheme(val error: Error) : + PublicationError(R.string.publication_error_scheme_not_supported) - class Forbidden(val error: Error? = null) : PublicationError( - R.string.publication_error_forbidden - ) + class UnsupportedContentProtection(val error: Error? = null) : + PublicationError(R.string.publication_error_unsupported_protection) + class UnsupportedArchiveFormat(val error: Error) : + PublicationError(R.string.publication_error_unsupported_archive) - class Unexpected(val error: Error) : PublicationError(R.string.publication_error_unexpected) + class UnsupportedPublication(val error: Error? = null) : + PublicationError(R.string.publication_error_unsupported_asset) - companion object { + class InvalidPublication(val error: Error) : + PublicationError(R.string.publication_error_invalid_publication) - operator fun invoke(error: AssetError): PublicationError = - when (error) { - is AssetError.Forbidden -> - Forbidden(error) - is AssetError.IncorrectCredentials -> - IncorrectCredentials(error) - is AssetError.NotFound -> - NotFound(error) - is AssetError.OutOfMemory -> - OutOfMemory(error) - is AssetError.InvalidAsset -> - InvalidPublication(error) - is AssetError.Unknown -> - Unexpected(error) - is AssetError.UnsupportedAsset -> - UnsupportedAsset(error) - is AssetError.Filesystem -> - Filesystem(error.cause) - is AssetError.Network -> - Network(error.cause) - } + class RestrictedPublication(val error: Error? = null) : + PublicationError(R.string.publication_error_restricted) + + class Unexpected(val error: Error) : + PublicationError(R.string.publication_error_unexpected) + + companion object { operator fun invoke(error: AssetRetriever.Error): PublicationError = when (error) { + is AssetRetriever.Error.AccessError -> + PublicationError(error.cause) is AssetRetriever.Error.ArchiveFormatNotSupported -> - UnsupportedAsset(error) - is AssetRetriever.Error.Forbidden -> - Forbidden(error) - is AssetRetriever.Error.NotFound -> - NotFound(error) - is AssetRetriever.Error.InvalidAsset -> - InvalidPublication(error) - is AssetRetriever.Error.OutOfMemory -> - OutOfMemory(error) + UnsupportedArchiveFormat(error) is AssetRetriever.Error.SchemeNotSupported -> - SchemeNotSupported(error) - is AssetRetriever.Error.Unknown -> - Unexpected(error) - is AssetRetriever.Error.Filesystem -> - Filesystem(error.cause) - is AssetRetriever.Error.Network -> - Filesystem(error.cause) + UnsupportedScheme(error) + } + + operator fun invoke(error: ContentProtectionSchemeRetriever.Error): PublicationError = + when (error) { + is ContentProtectionSchemeRetriever.Error.AccessError -> + PublicationError(error.cause) + ContentProtectionSchemeRetriever.Error.NoContentProtectionFound -> + UnsupportedContentProtection() + } + + operator fun invoke(error: PublicationFactory.Error): PublicationError = + when (error) { + is PublicationFactory.Error.ReadError -> + PublicationError(error.cause) + is PublicationFactory.Error.UnsupportedAsset -> + UnsupportedPublication(error) + is PublicationFactory.Error.UnsupportedContentProtection -> + UnsupportedContentProtection(error) + } + + operator fun invoke(error: ReadError): PublicationError = + when (error) { + is ReadError.Access -> + when (val cause = error.cause) { + is HttpError -> PublicationError(cause) + is FileSystemError -> PublicationError(cause) + is ContentProviderError -> PublicationError(cause) + else -> Unexpected(cause) + } + is ReadError.Decoding -> InvalidPublication(error) + is ReadError.Other -> Unexpected(error) + is ReadError.OutOfMemory -> OutOfMemory(error) + is ReadError.UnsupportedOperation -> Unexpected(error) + } + + private operator fun invoke(error: HttpError): PublicationError = + when (error) { + is HttpError.IO -> + HttpUnexpected(error) + is HttpError.MalformedResponse -> + HttpUnexpected(error) + is HttpError.Redirection -> + HttpUnexpected(error) + is HttpError.Timeout -> + HttpConnectivity(error) + is HttpError.UnreachableHost -> + HttpConnectivity(error) + is HttpError.Response -> + when (error.status) { + HttpStatus.Forbidden -> HttpForbidden(error) + HttpStatus.NotFound -> HttpNotFound(error) + else -> HttpUnexpected(error) + } + } + + private operator fun invoke(error: FileSystemError): PublicationError = + when (error) { + is FileSystemError.Forbidden -> FsUnexpected(error) + is FileSystemError.IO -> FsUnexpected(error) + is FileSystemError.NotFound -> FsNotFound(error) + } + + private operator fun invoke(error: ContentProviderError): PublicationError = + when (error) { + is ContentProviderError.FileNotFound -> FsNotFound(error) + is ContentProviderError.IO -> FsUnexpected(error) + is ContentProviderError.NotAvailable -> FsUnexpected(error) } } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index fa522c1deb..4ab9f0a244 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -6,6 +6,7 @@ package org.readium.r2.testapp.domain +import org.readium.r2.lcp.LcpPublicationRetriever as ReadiumLcpPublicationRetriever import android.content.Context import android.net.Uri import java.io.File @@ -14,17 +15,16 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import org.readium.r2.lcp.LcpException -import org.readium.r2.lcp.LcpPublicationRetriever as ReadiumLcpPublicationRetriever import org.readium.r2.lcp.LcpService import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.opds.images 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.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry @@ -123,7 +123,11 @@ class LocalPublicationRetriever( coroutineScope.launch { val tempFile = uri.copyToTempFile(context, storageDir) .getOrElse { - listener.onError(ImportError.ResourceError(ReadError.Filesystem(it))) + listener.onError( + ImportError.PublicationError( + PublicationError.FsUnexpected(FileSystemError.IO(it)) + ) + ) return@launch } @@ -161,7 +165,7 @@ class LocalPublicationRetriever( ) { if (lcpPublicationRetriever == null) { listener.onError( - ImportError.PublicationError(PublicationError.UnsupportedAsset()) + ImportError.PublicationError(PublicationError.UnsupportedContentProtection()) ) } else { lcpPublicationRetriever.retrieve(sourceAsset, tempFile, coverUrl) @@ -178,7 +182,9 @@ class LocalPublicationRetriever( } catch (e: Exception) { Timber.d(e) tryOrNull { libraryFile.delete() } - listener.onError(ImportError.ResourceError(ReadError.Filesystem(e))) + listener.onError(ImportError.PublicationError( + PublicationError.FsUnexpected(ThrowableError(e))) + ) return } @@ -253,12 +259,13 @@ class OpdsPublicationRetriever( } } - private fun Publication.acquisitionUrl(): Try { - val acquisitionLink = links - .firstOrNull { it.mediaType?.isPublication == true || it.mediaType == MediaType.LCP_LICENSE_DOCUMENT } + private fun Publication.acquisitionUrl(): Try { + val acquisitionUrl = links + .filter { it.mediaType?.isPublication == true || it.mediaType == MediaType.LCP_LICENSE_DOCUMENT } + .firstNotNullOfOrNull { it.url() as? AbsoluteUrl } ?: return Try.failure(Exception("No supported link to acquire publication.")) - return Try.success(acquisitionLink.url()) + return Try.success(acquisitionUrl) } private val downloadListener: DownloadListener = @@ -340,7 +347,7 @@ class LcpPublicationRetriever( coroutineScope.launch { val license = licenceAsset.resource.read() .getOrElse { - listener.onError(ImportError.ResourceError(it)) + listener.onError(ImportError.PublicationError(PublicationError(it))) return@launch } .let { diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt new file mode 100644 index 0000000000..db45447c4e --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.reader + +import androidx.annotation.StringRes +import org.readium.r2.shared.UserException +import org.readium.r2.shared.util.Error +import org.readium.r2.testapp.R + +sealed class OpeningError( + content: Content, + cause: Exception? +) : UserException(content, cause) { + + constructor(@StringRes userMessageId: Int) : + this(Content(userMessageId), null) + + constructor(cause: UserException) : + this(Content(cause), cause) + + class PublicationError( + override val cause: org.readium.r2.testapp.domain.PublicationError + ) : OpeningError(cause) + + class AudioEngineInitialization( + val error: Error + ) : OpeningError(R.string.opening_publication_audio_engine_initialization) +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt index e88ff1434b..a0ecde416f 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt @@ -7,7 +7,6 @@ package org.readium.r2.testapp.reader import android.app.Application -import androidx.annotation.StringRes import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences as JetpackPreferences import org.json.JSONObject @@ -18,13 +17,11 @@ import org.readium.navigator.media.tts.TtsNavigatorFactory import org.readium.r2.navigator.epub.EpubNavigatorFactory import org.readium.r2.navigator.pdf.PdfNavigatorFactory import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.allAreHtml import org.readium.r2.shared.publication.services.isRestricted import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.getOrElse import org.readium.r2.testapp.Readium import org.readium.r2.testapp.data.BookRepository @@ -50,41 +47,6 @@ class ReaderRepository( private val bookRepository: BookRepository, private val preferencesDataStore: DataStore ) { - sealed class OpeningError( - content: Content, - cause: Exception? - ) : UserException(content, cause) { - - constructor(@StringRes userMessageId: Int) : - this(Content(userMessageId), null) - - constructor(cause: UserException) : - this(Content(cause), cause) - - class PublicationError( - override val cause: UserException - ) : OpeningError(cause) { - - companion object { - - operator fun invoke( - error: AssetRetriever.Error - ): OpeningError = PublicationError( - org.readium.r2.testapp.domain.PublicationError( - error - ) - ) - - operator fun invoke( - error: AssetError - ): OpeningError = PublicationError( - org.readium.r2.testapp.domain.PublicationError( - error - ) - ) - } - } - } private val coroutineQueue: CoroutineQueue = CoroutineQueue() @@ -112,17 +74,33 @@ class ReaderRepository( book.url, book.mediaType, book.containerType - ).getOrElse { return Try.failure(OpeningError.PublicationError(it)) } + ).getOrElse { + return Try.failure( + OpeningError.PublicationError( + PublicationError(it) + ) + ) + } val publication = readium.publicationFactory.open( asset, contentProtectionScheme = book.drmScheme, allowUserInteraction = true - ).getOrElse { return Try.failure(OpeningError.PublicationError(it)) } + ).getOrElse { + return Try.failure( + OpeningError.PublicationError( + PublicationError(it) + ) + ) + } // The publication is protected with a DRM and not unlocked. if (publication.isRestricted) { - return Try.failure(OpeningError.PublicationError(PublicationError.Forbidden())) + return Try.failure( + OpeningError.PublicationError( + PublicationError.RestrictedPublication() + ) + ) } val initialLocator = book.progression @@ -139,7 +117,9 @@ class ReaderRepository( openImage(bookId, publication, initialLocator) else -> Try.failure( - OpeningError.PublicationError(PublicationError.UnsupportedAsset()) + OpeningError.PublicationError( + PublicationError.UnsupportedPublication() + ) ) } @@ -159,15 +139,22 @@ class ReaderRepository( publication, ExoPlayerEngineProvider(application) ) ?: return Try.failure( - OpeningError.PublicationError(PublicationError.UnsupportedAsset()) + OpeningError.PublicationError(PublicationError.UnsupportedPublication()) ) val navigator = navigatorFactory.createNavigator( initialLocator, initialPreferences - ) ?: return Try.failure( - OpeningError.PublicationError(PublicationError.UnsupportedAsset()) - ) + ).getOrElse { + return Try.failure( + when (it) { + is AudioNavigatorFactory.Error.EngineInitialization -> + OpeningError.AudioEngineInitialization(it) + is AudioNavigatorFactory.Error.UnsupportedPublication -> + OpeningError.PublicationError(PublicationError.UnsupportedPublication()) + } + ) + } mediaServiceFacade.openSession(bookId, navigator) val initData = MediaReaderInitData( diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt index 1f2d3af55f..7ce6aedb13 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt @@ -38,6 +38,7 @@ import org.readium.r2.shared.util.data.ReadError import org.readium.r2.testapp.Application import org.readium.r2.testapp.data.BookRepository import org.readium.r2.testapp.data.model.Highlight +import org.readium.r2.testapp.domain.PublicationError import org.readium.r2.testapp.reader.preferences.UserPreferencesViewModel import org.readium.r2.testapp.reader.tts.TtsViewModel import org.readium.r2.testapp.search.SearchPagingSource @@ -271,11 +272,11 @@ class ReaderViewModel( // Navigator.Listener override fun onResourceLoadFailed(href: Url, error: ReadError) { - val message = when (error) { - is ReadError.OutOfMemory -> "The resource is too large to be rendered on this device: $href" - else -> "Failed to render the resource: $href" - } - activityChannel.send(ActivityCommand.ToastError(UserException(message, error))) + activityChannel.send( + ActivityCommand.ToastError( + PublicationError(error) + ) + ) } // HyperlinkNavigator.Listener diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt index 6bf615667f..350bb4ce17 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt @@ -25,6 +25,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Language +import org.readium.r2.shared.util.getOrElse import org.readium.r2.testapp.domain.TtsError import org.readium.r2.testapp.reader.MediaService import org.readium.r2.testapp.reader.MediaServiceFacade @@ -180,7 +181,7 @@ class TtsViewModel private constructor( this, initialLocator = start, initialPreferences = preferencesManager.preferences.value - ) ?: run { + ).getOrElse { val error = TtsError.Initialization() _events.send(Event.OnError(error)) return diff --git a/test-app/src/main/res/values/strings.xml b/test-app/src/main/res/values/strings.xml index c55377e196..e9506fc97c 100644 --- a/test-app/src/main/res/values/strings.xml +++ b/test-app/src/main/res/values/strings.xml @@ -91,6 +91,8 @@ This will return the publication Return + Could not open publication because audio engine initialization failed. + Unable to add publication due to an unexpected error on your device Publication download failed. Acquisition is not possible. @@ -99,14 +101,18 @@ Unable to add publication to the database Asset format is not supported - Publication has not been found. + Server denied access to a resource. + A resource has not been found on the server. + A connectivity error occurred. An unexpected network error occurred. - An unexpected filesystem error occurred. - Publication is temporarily unavailable. + A file has not been found. + An unexpected filesystem error occurred. Provided credentials were incorrect - You are not allowed to open this publication + You are not allowed to open this publication There is not enough memory on this device to open the publication. - Publication source not supported. + Archive format is not supported. + Content protection is not supported. + Publication source is not supported. Publication looks corrupted. An unexpected error occurred. From dbd73a5bf9c81bf16c0cdc38b9d1a22378ce320d Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Fri, 17 Nov 2023 14:07:38 +0100 Subject: [PATCH 13/86] Lint --- .../adapter/pspdfkit/document/PsPdfKitDocument.kt | 2 +- .../main/java/org/readium/r2/navigator/Navigator.kt | 1 - .../org/readium/r2/navigator/media/ExoMediaPlayer.kt | 2 +- .../org/readium/navigator/media/tts/TtsNavigator.kt | 8 -------- .../navigator/media/tts/session/TtsSessionAdapter.kt | 2 +- .../org/readium/r2/shared/publication/Publication.kt | 2 +- .../publication/services/ContentProtectionService.kt | 4 ++-- .../r2/shared/publication/services/CoverService.kt | 4 ++-- .../r2/shared/publication/services/PositionsService.kt | 2 +- .../readium/r2/shared/util/archive/ArchiveProvider.kt | 2 +- .../r2/shared/util/archive/FileZipArchiveProvider.kt | 4 ++-- .../archive/{ZipContainer.kt => FileZipContainer.kt} | 2 +- .../java/org/readium/r2/shared/util/data/Container.kt | 1 - .../java/org/readium/r2/shared/util/data/ReadError.kt | 4 ++-- .../util/downloads/android/AndroidDownloadManager.kt | 4 ++-- .../downloads/foreground/ForegroundDownloadManager.kt | 2 +- .../readium/r2/shared/util/http/DefaultHttpClient.kt | 2 +- .../java/org/readium/r2/shared/util/http/HttpError.kt | 4 ++-- .../org/readium/r2/shared/util/http/HttpResource.kt | 4 +++- .../r2/shared/util/zip/StreamingZipArchiveProvider.kt | 2 +- .../org/readium/r2/streamer/parser/epub/EpubParser.kt | 2 +- .../src/main/java/org/readium/r2/testapp/Readium.kt | 4 ++-- .../readium/r2/testapp/catalogs/CatalogViewModel.kt | 2 +- .../java/org/readium/r2/testapp/domain/CoverStorage.kt | 2 +- .../r2/testapp/{reader => domain}/OpeningError.kt | 2 +- .../org/readium/r2/testapp/domain/PublicationError.kt | 2 +- .../readium/r2/testapp/domain/PublicationRetriever.kt | 10 ++++++---- .../java/org/readium/r2/testapp/domain/ReaderError.kt | 1 - .../org/readium/r2/testapp/reader/ReaderRepository.kt | 1 + 29 files changed, 39 insertions(+), 45 deletions(-) rename readium/shared/src/main/java/org/readium/r2/shared/util/archive/{ZipContainer.kt => FileZipContainer.kt} (99%) rename test-app/src/main/java/org/readium/r2/testapp/{reader => domain}/OpeningError.kt (95%) delete mode 100644 test-app/src/main/java/org/readium/r2/testapp/domain/ReaderError.kt diff --git a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt index 3a4f8ec0a9..761039111f 100644 --- a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt @@ -6,13 +6,13 @@ package org.readium.adapter.pspdfkit.document -import com.pspdfkit.document.PdfDocument as _PsPdfKitDocument import android.content.Context import android.graphics.Bitmap import com.pspdfkit.annotations.actions.GoToAction import com.pspdfkit.document.DocumentSource import com.pspdfkit.document.OutlineElement import com.pspdfkit.document.PageBinding +import com.pspdfkit.document.PdfDocument as _PsPdfKitDocument import com.pspdfkit.document.PdfDocumentLoader import com.pspdfkit.exceptions.InvalidPasswordException import com.pspdfkit.exceptions.InvalidSignatureException diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt index d2e3f892f5..77d2819224 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/Navigator.kt @@ -9,7 +9,6 @@ package org.readium.r2.navigator import kotlinx.coroutines.flow.StateFlow import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ReadError diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt index 8ea2038801..a1688c40a8 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt @@ -56,8 +56,8 @@ import org.readium.r2.shared.publication.PublicationId import org.readium.r2.shared.publication.indexOfFirstWithHref import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.toUri import timber.log.Timber diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt index 2c7455a7da..842b37dd2c 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt @@ -6,15 +6,10 @@ package org.readium.navigator.media.tts -import android.app.Application -import androidx.media3.common.MediaItem -import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.StateFlow import org.readium.navigator.media.common.Media3Adapter -import org.readium.navigator.media.common.MediaMetadataProvider import org.readium.navigator.media.common.MediaNavigator import org.readium.navigator.media.common.TextAwareMediaNavigator import org.readium.navigator.media.tts.session.TtsSessionAdapter @@ -27,10 +22,7 @@ import org.readium.r2.shared.extensions.mapStateIn import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.services.content.ContentService -import org.readium.r2.shared.util.Language import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.tokenizer.TextTokenizer /** * A navigator to read aloud a [Publication] with a TTS engine. diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt index 2755d96fad..59f54f6f6c 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt @@ -44,8 +44,8 @@ import org.readium.navigator.media.tts.TtsEngine import org.readium.navigator.media.tts.TtsPlayer import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.ErrorException -import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.http.HttpError /** * Adapts the [TtsPlayer] to media3 [Player] interface. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index d6806a2009..4f8072fc28 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -34,8 +34,8 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.EmptyContainer -import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpStreamResponse import org.readium.r2.shared.util.mediatype.MediaType diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt index faec271212..2802aafc37 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt @@ -23,9 +23,9 @@ import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.http.HttpError -import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpResponse +import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.shared.util.http.HttpStreamResponse import org.readium.r2.shared.util.mediatype.MediaType @@ -405,7 +405,7 @@ private fun forbiddenResponse(): HttpError.Response = private fun badRequestResponse(detail: String): HttpError.Response = HttpError.Response( - HttpStatus.BadRequest, + HttpStatus.BadRequest, MediaType.JSON_PROBLEM_DETAILS, JSONObject().apply { put("title", "Bad request") diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index 607c9072e5..d312b9d4ed 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -22,13 +22,13 @@ 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.data.Container -import org.readium.r2.shared.util.http.HttpError -import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.shared.util.data.readAsBitmap import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpResponse +import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.shared.util.http.HttpStreamResponse import org.readium.r2.shared.util.http.fetch import org.readium.r2.shared.util.mediatype.MediaType diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt index f606ef3170..dd369ec297 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt @@ -25,8 +25,8 @@ import org.readium.r2.shared.toJSON 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.http.HttpError import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpResponse import org.readium.r2.shared.util.http.HttpStreamResponse diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt index 6cf61803ae..81f552e36b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt @@ -13,8 +13,8 @@ import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaTypeSniffer -import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceContainer public interface ArchiveProvider : MediaTypeSniffer, ArchiveFactory diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt index 547519d1f3..a84f9d6a55 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt @@ -49,7 +49,7 @@ public class FileZipArchiveProvider( return withContext(Dispatchers.IO) { try { - JavaZipContainer(ZipFile(file), file, mediaTypeRetriever) + FileZipContainer(ZipFile(file), file, mediaTypeRetriever) Try.success(MediaType.ZIP) } catch (e: ZipException) { Try.failure(MediaTypeSnifferError.NotRecognized) @@ -94,7 +94,7 @@ public class FileZipArchiveProvider( internal suspend fun open(file: File): Try, ArchiveFactory.Error> = withContext(Dispatchers.IO) { try { - val archive = JavaZipContainer(ZipFile(file), file, mediaTypeRetriever) + val archive = FileZipContainer(ZipFile(file), file, mediaTypeRetriever) Try.success(archive) } catch (e: FileNotFoundException) { Try.failure( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt similarity index 99% rename from readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt index 20320b92ba..02f70d9cb4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt @@ -33,7 +33,7 @@ import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.tryRecover -internal class JavaZipContainer( +internal class FileZipContainer( private val archive: ZipFile, file: File, private val mediaTypeRetriever: MediaTypeRetriever diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index 75573e3ff7..37076c58f8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -36,7 +36,6 @@ public interface Container : Iterable, SuspendingCloseable { * fetching it, such as HTTP. Therefore, errors are handled at the Entry level. */ public operator fun get(url: Url): E? - } /** A [Container] providing no resources at all. */ diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt index 22d649d15e..dcc6f3218e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt @@ -40,8 +40,8 @@ public sealed class ReadError( public class UnsupportedOperation(cause: Error? = null) : ReadError("Could not proceed because an operation was not supported.", cause) { - public constructor(message: String) : this(MessageError(message)) - } + public constructor(message: String) : this(MessageError(message)) + } /** For any other error, such as HTTP 500. */ public class Other(cause: Error) : diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index 041beba387..474087916a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -27,10 +27,10 @@ import org.readium.r2.shared.extensions.tryOr import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.FileBlob -import org.readium.r2.shared.util.http.HttpError -import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt index 08634cd353..9b2cda3d82 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt @@ -19,10 +19,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpResponse import org.readium.r2.shared.util.http.HttpTry diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index 69bed530ce..7079b32a14 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -208,7 +208,7 @@ public class DefaultHttpClient( ) } } catch (e: IOException) { - Try.failure( wrap(e)) + Try.failure(wrap(e)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt index 33b3fa91ba..8b4345928c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt @@ -37,8 +37,8 @@ public sealed class HttpError( public class IO(cause: Error) : HttpError("An IO error occurred.", cause) { - public constructor(exception: Exception) : this(ThrowableError(exception)) - } + public constructor(exception: Exception) : this(ThrowableError(exception)) + } /** * @param status HTTP status code. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index e119de06f0..bd24b8220d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -38,7 +38,9 @@ public class HttpResource( } else { Try.failure( ReadError.UnsupportedOperation( - MessageError("Server did not provide content length in its response to request to $source.") + MessageError( + "Server did not provide content length in its response to request to $source." + ) ) ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 3df1955525..077aaa5f6b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -23,8 +23,8 @@ 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 1e8ca205da..4f53bff966 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -110,7 +110,7 @@ public class EpubParser( } private suspend fun getRootFilePath(container: Container): Try { - val containerXmlUrl =Url("META-INF/container.xml")!! + val containerXmlUrl = Url("META-INF/container.xml")!! val containerXmlResource = container .get(containerXmlUrl) diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 3a5437df95..ff0bfd0da2 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -18,13 +18,13 @@ import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetri import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.asset.CompositeResourceFactory -import org.readium.r2.shared.util.asset.HttpResourceFactory +import org.readium.r2.shared.util.asset.ContentResourceFactory import org.readium.r2.shared.util.asset.FileResourceFactory +import org.readium.r2.shared.util.asset.HttpResourceFactory import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaTypeRetriever -import org.readium.r2.shared.util.asset.ContentResourceFactory import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider import org.readium.r2.streamer.PublicationFactory diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogViewModel.kt index 954642a6cd..3128c1dd17 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogViewModel.kt @@ -31,7 +31,7 @@ class CatalogViewModel(application: Application) : AndroidViewModel(application) fun parseCatalog(catalog: Catalog) = viewModelScope.launch { var parseRequest: Try? = null - catalog.href.let {href -> + catalog.href.let { href -> AbsoluteUrl(href) ?.let { HttpRequest(it) } ?.let { request -> diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt index 3b9378aa86..27c95de8e0 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/CoverStorage.kt @@ -11,8 +11,8 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.cover import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.fetchWithDecoder import org.readium.r2.testapp.utils.tryOrLog diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/OpeningError.kt similarity index 95% rename from test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt rename to test-app/src/main/java/org/readium/r2/testapp/domain/OpeningError.kt index db45447c4e..7494f42615 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/OpeningError.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.testapp.reader +package org.readium.r2.testapp.domain import androidx.annotation.StringRes import org.readium.r2.shared.UserException diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index 1a151ae04f..e4c37c6cc0 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -93,7 +93,7 @@ sealed class PublicationError(@StringRes userMessageId: Int) : UserException(use } operator fun invoke(error: ReadError): PublicationError = - when (error) { + when (error) { is ReadError.Access -> when (val cause = error.cause) { is HttpError -> PublicationError(cause) diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index 4ab9f0a244..0f7cdddc03 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -6,7 +6,6 @@ package org.readium.r2.testapp.domain -import org.readium.r2.lcp.LcpPublicationRetriever as ReadiumLcpPublicationRetriever import android.content.Context import android.net.Uri import java.io.File @@ -15,6 +14,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import org.readium.r2.lcp.LcpException +import org.readium.r2.lcp.LcpPublicationRetriever as ReadiumLcpPublicationRetriever import org.readium.r2.lcp.LcpService import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.publication.Publication @@ -125,7 +125,7 @@ class LocalPublicationRetriever( .getOrElse { listener.onError( ImportError.PublicationError( - PublicationError.FsUnexpected(FileSystemError.IO(it)) + PublicationError.FsUnexpected(FileSystemError.IO(it)) ) ) return@launch @@ -182,8 +182,10 @@ class LocalPublicationRetriever( } catch (e: Exception) { Timber.d(e) tryOrNull { libraryFile.delete() } - listener.onError(ImportError.PublicationError( - PublicationError.FsUnexpected(ThrowableError(e))) + listener.onError( + ImportError.PublicationError( + PublicationError.FsUnexpected(ThrowableError(e)) + ) ) return } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ReaderError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ReaderError.kt deleted file mode 100644 index 5dcee2b97e..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ReaderError.kt +++ /dev/null @@ -1 +0,0 @@ -package org.readium.r2.testapp.domain diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt index a0ecde416f..5cdacfca39 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt @@ -25,6 +25,7 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrElse import org.readium.r2.testapp.Readium import org.readium.r2.testapp.data.BookRepository +import org.readium.r2.testapp.domain.OpeningError import org.readium.r2.testapp.domain.PublicationError import org.readium.r2.testapp.reader.preferences.AndroidTtsPreferencesManagerFactory import org.readium.r2.testapp.reader.preferences.EpubPreferencesManagerFactory From 2ce91e0f3b32cf769ed84aa0b5ee9e3c093b4888 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Sun, 19 Nov 2023 10:19:52 +0100 Subject: [PATCH 14/86] Refactor LcpException and move UserError to test-app --- .../readium/r2/lcp/LcpContentProtection.kt | 12 +- .../r2/lcp/LcpContentProtectionService.kt | 4 +- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 5 +- .../main/java/org/readium/r2/lcp/LcpError.kt | 262 +++++++++++++++ .../java/org/readium/r2/lcp/LcpException.kt | 283 ---------------- .../java/org/readium/r2/lcp/LcpLicense.kt | 13 +- .../readium/r2/lcp/LcpPublicationRetriever.kt | 8 +- .../java/org/readium/r2/lcp/LcpService.kt | 12 +- .../org/readium/r2/lcp/license/License.kt | 46 ++- .../r2/lcp/license/LicenseValidation.kt | 35 +- .../container/ContainerLicenseContainer.kt | 5 +- .../container/ContentZipLicenseContainer.kt | 7 +- .../container/FileZipLicenseContainer.kt | 9 +- .../license/container/LcplLicenseContainer.kt | 5 +- .../container/LcplResourceLicenseContainer.kt | 3 +- .../lcp/license/container/LicenseContainer.kt | 3 +- .../r2/lcp/license/model/LicenseDocument.kt | 49 ++- .../r2/lcp/license/model/StatusDocument.kt | 27 +- .../r2/lcp/license/model/components/Link.kt | 5 +- .../model/components/lcp/ContentKey.kt | 17 +- .../model/components/lcp/Encryption.kt | 25 +- .../license/model/components/lcp/Signature.kt | 13 +- .../license/model/components/lcp/UserKey.kt | 25 +- .../org/readium/r2/lcp/public/Deprecated.kt | 4 +- .../org/readium/r2/lcp/service/CRLService.kt | 3 +- .../r2/lcp/service/DeviceRepository.kt | 5 +- .../org/readium/r2/lcp/service/LcpClient.kt | 27 +- .../readium/r2/lcp/service/LicensesService.kt | 14 +- .../readium/r2/lcp/service/NetworkService.kt | 5 +- readium/lcp/src/main/res/values/strings.xml | 45 --- .../readium/navigator/media/tts/TtsEngine.kt | 3 +- .../navigator/media/tts/TtsNavigator.kt | 19 +- .../media/tts/TtsNavigatorFactory.kt | 28 +- .../readium/navigator/media/tts/TtsPlayer.kt | 4 +- .../media/tts/android/AndroidTtsEngine.kt | 25 +- .../protection/ContentProtection.kt | 41 +-- .../ContentProtectionSchemeRetriever.kt | 3 +- .../FallbackContentProtectionService.kt | 26 +- .../services/ContentProtectionService.kt | 25 +- .../r2/shared/util/asset/AssetRetriever.kt | 12 +- .../r2/shared/util/asset/ResourceFactory.kt | 7 +- .../readium/r2/shared/util/data/Decoding.kt | 8 +- .../readium/r2/streamer/PublicationFactory.kt | 2 +- .../r2/streamer/parser/PublicationParser.kt | 3 +- .../org/readium/r2/testapp/MainActivity.kt | 8 +- .../java/org/readium/r2/testapp/Readium.kt | 5 +- .../r2/testapp/bookshelf/BookshelfFragment.kt | 7 +- .../testapp/bookshelf/BookshelfViewModel.kt | 8 +- .../readium/r2/testapp/domain/Bookshelf.kt | 10 +- .../readium/r2/testapp/domain/ImportError.kt | 32 +- .../r2/testapp/domain/ImportUserError.kt | 61 ++++ .../readium/r2/testapp/domain/LcpUserError.kt | 313 ++++++++++++++++++ .../readium/r2/testapp/domain/OpeningError.kt | 32 -- .../r2/testapp/domain/PublicationError.kt | 119 ++----- .../r2/testapp/domain/PublicationRetriever.kt | 43 ++- .../r2/testapp/domain/PublicationUserError.kt | 68 ++++ .../r2/testapp/domain/ReadUserError.kt | 104 ++++++ .../readium/r2/testapp/domain/SearchError.kt | 27 -- .../org/readium/r2/testapp/domain/TtsError.kt | 30 -- .../r2/testapp/drm/DrmManagementFragment.kt | 15 +- .../r2/testapp/drm/DrmManagementViewModel.kt | 10 +- .../r2/testapp/drm/LcpManagementViewModel.kt | 5 +- .../r2/testapp/reader/BaseReaderFragment.kt | 8 +- .../readium/r2/testapp/reader/OpeningError.kt | 29 ++ .../r2/testapp/reader/OpeningUserError.kt | 49 +++ .../r2/testapp/reader/ReaderActivity.kt | 10 +- .../r2/testapp/reader/ReaderRepository.kt | 20 +- .../r2/testapp/reader/ReaderViewModel.kt | 42 ++- .../r2/testapp/reader/VisualReaderFragment.kt | 10 +- .../readium/r2/testapp/reader/tts/TtsError.kt | 40 +++ .../r2/testapp/reader/tts/TtsUserError.kt | 60 ++++ .../r2/testapp/reader/tts/TtsViewModel.kt | 17 +- .../r2/testapp/search/SearchUserError.kt | 64 ++++ .../org/readium/r2/testapp/utils/UserError.kt | 81 ++--- .../utils/extensions/readium/ErrorExt.kt | 31 +- test-app/src/main/res/values/strings.xml | 49 +++ 76 files changed, 1671 insertions(+), 918 deletions(-) create mode 100644 readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt delete mode 100644 readium/lcp/src/main/java/org/readium/r2/lcp/LcpException.kt create mode 100644 test-app/src/main/java/org/readium/r2/testapp/domain/ImportUserError.kt create mode 100644 test-app/src/main/java/org/readium/r2/testapp/domain/LcpUserError.kt delete mode 100644 test-app/src/main/java/org/readium/r2/testapp/domain/OpeningError.kt create mode 100644 test-app/src/main/java/org/readium/r2/testapp/domain/PublicationUserError.kt create mode 100644 test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt delete mode 100644 test-app/src/main/java/org/readium/r2/testapp/domain/SearchError.kt delete mode 100644 test-app/src/main/java/org/readium/r2/testapp/domain/TtsError.kt create mode 100644 test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt create mode 100644 test-app/src/main/java/org/readium/r2/testapp/reader/OpeningUserError.kt create mode 100644 test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsError.kt create mode 100644 test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsUserError.kt create mode 100644 test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt rename readium/shared/src/main/java/org/readium/r2/shared/UserException.kt => test-app/src/main/java/org/readium/r2/testapp/utils/UserError.kt (56%) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index 5874d56092..3b86f4247f 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -64,7 +64,7 @@ internal class LcpContentProtection( asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { val authentication = credentials ?.let { LcpPassphraseAuthentication(it, fallback = this.authentication) } ?: this.authentication @@ -74,7 +74,7 @@ internal class LcpContentProtection( private fun createResultAsset( asset: Asset.Container, - license: Try + license: Try ): Try { val serviceFactory = LcpContentProtectionService .createFactory(license.getOrNull(), license.failureOrNull()) @@ -115,7 +115,7 @@ internal class LcpContentProtection( LicenseDocument(it) } catch (e: Exception) { return Try.failure( - ContentProtection.Error.AccessError( + ContentProtection.Error.ReadError( ReadError.Decoding( MessageError( "Failed to read the LCP license document", @@ -128,14 +128,14 @@ internal class LcpContentProtection( } .getOrElse { return Try.failure( - ContentProtection.Error.AccessError(it) + ContentProtection.Error.ReadError(it) ) } val link = licenseDoc.publicationLink val url = (link.url() as? AbsoluteUrl) ?: return Try.failure( - ContentProtection.Error.AccessError( + ContentProtection.Error.ReadError( ReadError.Decoding( MessageError( "The LCP license document does not contain a valid link to the publication" @@ -179,7 +179,7 @@ internal class LcpContentProtection( is AssetRetriever.Error.ArchiveFormatNotSupported -> ContentProtection.Error.UnsupportedAsset(this) is AssetRetriever.Error.AccessError -> - ContentProtection.Error.AccessError(cause) + ContentProtection.Error.ReadError(cause) is AssetRetriever.Error.SchemeNotSupported -> ContentProtection.Error.UnsupportedAsset(this) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtectionService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtectionService.kt index d11eed1172..91891d0850 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtectionService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtectionService.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.publication.services.ContentProtectionService public class LcpContentProtectionService( public val license: LcpLicense?, - override val error: LcpException? + override val error: LcpError? ) : ContentProtectionService { override val isRestricted: Boolean = license == null @@ -33,7 +33,7 @@ public class LcpContentProtectionService( public companion object { - public fun createFactory(license: LcpLicense?, error: LcpException?): ( + public fun createFactory(license: LcpLicense?, error: LcpError?): ( Publication.Service.Context ) -> LcpContentProtectionService = { LcpContentProtectionService(license, error) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index cb5f9667ed..03501f6c5b 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -15,7 +15,6 @@ import org.readium.r2.shared.extensions.requireLengthFitInt import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError -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.assertSuccess @@ -250,7 +249,7 @@ internal class LcpDecryptor( ReadError.Decoding( MessageError( "Can't decrypt the content for resource with key: ${resource.source}", - ThrowableError(it) + it ) ) ) @@ -311,7 +310,7 @@ private suspend fun LcpLicense.decryptFully( .getOrElse { return Try.failure( ReadError.Decoding( - MessageError("Failed to decrypt the resource", ThrowableError(it)) + MessageError("Failed to decrypt the resource", it) ) ) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt new file mode 100644 index 0000000000..ca61966568 --- /dev/null +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt @@ -0,0 +1,262 @@ +/* + * Copyright 2020 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.lcp + +import java.net.SocketTimeoutException +import java.util.* +import org.readium.r2.lcp.service.NetworkException +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.Url + +public sealed class LcpError( + override val message: String, + override val cause: Error? = null +) : Error { + + /** The interaction is not available with this License. */ + public object LicenseInteractionNotAvailable : + LcpError("This interaction is not available.") + + /** This License's profile is not supported by liblcp. */ + public object LicenseProfileNotSupported : + LcpError( + "This License has a profile identifier that this app cannot handle, the publication cannot be processed" + ) + + /** Failed to retrieve the Certificate Revocation List. */ + public object CrlFetching : + LcpError("Can't retrieve the Certificate Revocation List") + + /** A network request failed with the given exception. */ + public class Network(override val cause: Error?) : + LcpError("NetworkError", cause = cause) { + + internal constructor(throwable: Throwable) : this(ThrowableError(throwable)) + } + + /** + * An unexpected LCP exception occurred. Please post an issue on r2-lcp-kotlin with the error + * message and how to reproduce it. + */ + public class Runtime(message: String) : + LcpError("Unexpected LCP error", MessageError(message)) + + /** An unknown low-level exception was reported. */ + public class Unknown(override val cause: Error?) : + LcpError("Unknown LCP error") { + + internal constructor(throwable: Throwable) : this(ThrowableError(throwable)) + } + + /** + * Errors while checking the status of the License, using the Status Document. + * + * The app should notify the user and stop there. The message to the user must be clear about + * the status of the license: don't display "expired" if the status is "revoked". The date and + * time corresponding to the new status should be displayed (e.g. "The license expired on 01 + * January 2018"). + */ + public sealed class LicenseStatus( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + public class Cancelled(public val date: Date) : + LicenseStatus("This license was cancelled on $date") + + public class Returned(public val date: Date) : + LicenseStatus("This license has been returned on $date") + + public class NotStarted(public val start: Date) : + LicenseStatus("This license starts on $start") + + public class Expired(public val end: Date) : + LicenseStatus("This license expired on $end") + + /** + * If the license has been revoked, the user message should display the number of devices which + * registered to the server. This count can be calculated from the number of "register" events + * in the status document. If no event is logged in the status document, no such message should + * appear (certainly not "The license was registered by 0 devices"). + */ + public class Revoked(public val date: Date, public val devicesCount: Int) : + LicenseStatus( + "This license was revoked by its provider on $date. It was registered by $devicesCount device(s)." + ) + } + + /** + * Errors while renewing a loan. + */ + public sealed class Renew( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + /** Your publication could not be renewed properly. */ + public object RenewFailed : + Renew("Publication could not be renewed properly") + + /** Incorrect renewal period, your publication could not be renewed. */ + public class InvalidRenewalPeriod(public val maxRenewDate: Date?) : + Renew("Incorrect renewal period, your publication could not be renewed") + + /** An unexpected error has occurred on the licensing server. */ + public object UnexpectedServerError : + Renew("An unexpected error has occurred on the server") + } + + /** + * Errors while returning a loan. + */ + public sealed class Return( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + /** Your publication could not be returned properly. */ + public object ReturnFailed : + Return("Publication could not be returned properly") + + /** Your publication has already been returned before or is expired. */ + + public object AlreadyReturnedOrExpired : + Return("Publication has already been returned before or is expired") + + /** An unexpected error has occurred on the licensing server. */ + public object UnexpectedServerError : + Return("An unexpected error has occurred on the server") + } + + /** + * Errors while parsing the License or Status JSON Documents. + */ + public sealed class Parsing( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + /** The JSON is malformed and can't be parsed. */ + public object MalformedJSON : + Parsing("The JSON is malformed and can't be parsed") + + /** The JSON is not representing a valid License Document. */ + public object LicenseDocument : + Parsing("The JSON is not representing a valid License Document") + + /** The JSON is not representing a valid Status Document. */ + public object StatusDocument : + Parsing("The JSON is not representing a valid Status Document") + + /** Invalid Link. */ + public object Link : + Parsing("The JSON is not representing a valid document") + + /** Invalid Encryption. */ + public object Encryption : + Parsing("The JSON is not representing a valid document") + + /** Invalid License Document Signature. */ + public object Signature : + Parsing("The JSON is not representing a valid document") + + /** Invalid URL for link with [rel]. */ + public class Url(public val rel: String) : + Parsing("The JSON is not representing a valid document") + } + + /** + * Errors while reading or writing a LCP container (LCPL, EPUB, LCPDF, etc.) + */ + public sealed class Container( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + /** Can't access the container, it's format is wrong. */ + public object OpenFailed : + Container("Can't open the license container") + + /** The file at given relative path is not found in the Container. */ + public class FileNotFound(public val url: Url) : + Container("License not found in container") + + /** Can't read the file at given relative path in the Container. */ + public class ReadFailed(public val url: Url?) : + Container("Can't read license from container") + + /** Can't write the file at given relative path in the Container. */ + public class WriteFailed(public val url: Url?) : + Container("Can't write license in container") + } + + /** + * An error occurred while checking the integrity of the License, it can't be retrieved. + */ + public sealed class LicenseIntegrity( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + public object CertificateRevoked : + LicenseIntegrity("Certificate has been revoked in the CRL") + + public object InvalidCertificateSignature : + LicenseIntegrity("Certificate has not been signed by CA") + + public object InvalidLicenseSignatureDate : + LicenseIntegrity("License has been issued by an expired certificate") + + public object InvalidLicenseSignature : + LicenseIntegrity("License signature does not match") + + public object InvalidUserKeyCheck : + LicenseIntegrity("User key check invalid") + } + + public sealed class Decryption( + message: String, + cause: Error? = null + ) : LcpError(message, cause) { + + public object ContentKeyDecryptError : + Decryption("Unable to decrypt encrypted content key from user key") + + public object ContentDecryptError : Decryption( + "Unable to decrypt encrypted content from content key" + ) + } + + public companion object { + + internal fun wrap(e: Exception): LcpError = when (e) { + is LcpException -> e.error + is NetworkException -> Network(e) + is SocketTimeoutException -> Network(e) + else -> Unknown(e) + } + } +} + +internal class LcpException(val error: LcpError) : Exception(error.message, ErrorException(error)) + +@Deprecated( + "Renamed to `LcpException`", + replaceWith = ReplaceWith("LcpException"), + level = DeprecationLevel.ERROR +) +public typealias LCPError = LcpError + +@Deprecated( + "Use `getUserMessage()` instead", + replaceWith = ReplaceWith("getUserMessage(context)"), + level = DeprecationLevel.ERROR +) +public val LcpError.errorDescription: String? get() = message diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpException.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpException.kt deleted file mode 100644 index cdec0c0df7..0000000000 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpException.kt +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright 2020 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.lcp - -import androidx.annotation.PluralsRes -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, - vararg args: Any, - quantity: Int? = null, - cause: Throwable? = null -) : UserException(userMessageId, quantity, *args, cause = cause) { - protected constructor(@StringRes userMessageId: Int, vararg args: Any, cause: Throwable? = null) : this( - userMessageId, - *args, - quantity = null, - cause = cause - ) - protected constructor( - @PluralsRes userMessageId: Int, - quantity: Int, - vararg args: Any, - cause: Throwable? = null - ) : this(userMessageId, *args, quantity = quantity, cause = cause) - - /** The interaction is not available with this License. */ - public object LicenseInteractionNotAvailable : LcpException( - R.string.readium_lcp_exception_license_interaction_not_available - ) - - /** This License's profile is not supported by liblcp. */ - public object LicenseProfileNotSupported : LcpException( - R.string.readium_lcp_exception_license_profile_not_supported - ) - - /** Failed to retrieve the Certificate Revocation List. */ - public object CrlFetching : LcpException(R.string.readium_lcp_exception_crl_fetching) - - /** A network request failed with the given exception. */ - public class Network(override val cause: Throwable?) : LcpException( - R.string.readium_lcp_exception_network, - cause = cause - ) - - /** - * An unexpected LCP exception occurred. Please post an issue on r2-lcp-kotlin with the error - * message and how to reproduce it. - */ - public class Runtime(override val message: String) : LcpException( - R.string.readium_lcp_exception_runtime - ) - - /** An unknown low-level exception was reported. */ - public class Unknown(override val cause: Throwable?) : LcpException( - R.string.readium_lcp_exception_unknown - ) - - /** - * Errors while checking the status of the License, using the Status Document. - * - * The app should notify the user and stop there. The message to the user must be clear about - * the status of the license: don't display "expired" if the status is "revoked". The date and - * time corresponding to the new status should be displayed (e.g. "The license expired on 01 - * January 2018"). - */ - public sealed class LicenseStatus(userMessageId: Int, vararg args: Any, quantity: Int? = null) : LcpException( - userMessageId, - *args, - quantity = quantity - ) { - protected constructor(@StringRes userMessageId: Int, vararg args: Any) : this( - userMessageId, - *args, - quantity = null - ) - protected constructor(@PluralsRes userMessageId: Int, quantity: Int, vararg args: Any) : this( - userMessageId, - *args, - quantity = quantity - ) - - public class Cancelled(public val date: Date) : LicenseStatus( - R.string.readium_lcp_exception_license_status_cancelled, - date - ) - - public class Returned(public val date: Date) : LicenseStatus( - R.string.readium_lcp_exception_license_status_returned, - date - ) - - public class NotStarted(public val start: Date) : LicenseStatus( - R.string.readium_lcp_exception_license_status_not_started, - start - ) - - public class Expired(public val end: Date) : LicenseStatus( - R.string.readium_lcp_exception_license_status_expired, - end - ) - - /** - * If the license has been revoked, the user message should display the number of devices which - * registered to the server. This count can be calculated from the number of "register" events - * in the status document. If no event is logged in the status document, no such message should - * appear (certainly not "The license was registered by 0 devices"). - */ - public class Revoked(public val date: Date, public val devicesCount: Int) : - LicenseStatus( - R.plurals.readium_lcp_exception_license_status_revoked, - devicesCount, - date, - devicesCount - ) - } - - /** - * Errors while renewing a loan. - */ - public sealed class Renew(@StringRes userMessageId: Int) : LcpException(userMessageId) { - - /** Your publication could not be renewed properly. */ - public object RenewFailed : Renew(R.string.readium_lcp_exception_renew_renew_failed) - - /** Incorrect renewal period, your publication could not be renewed. */ - public class InvalidRenewalPeriod(public val maxRenewDate: Date?) : Renew( - R.string.readium_lcp_exception_renew_invalid_renewal_period - ) - - /** An unexpected error has occurred on the licensing server. */ - public object UnexpectedServerError : Renew( - R.string.readium_lcp_exception_renew_unexpected_server_error - ) - } - - /** - * Errors while returning a loan. - */ - public sealed class Return(@StringRes userMessageId: Int) : LcpException(userMessageId) { - - /** Your publication could not be returned properly. */ - public object ReturnFailed : Return(R.string.readium_lcp_exception_return_return_failed) - - /** Your publication has already been returned before or is expired. */ - - public object AlreadyReturnedOrExpired : Return( - R.string.readium_lcp_exception_return_already_returned_or_expired - ) - - /** An unexpected error has occurred on the licensing server. */ - public object UnexpectedServerError : Return( - R.string.readium_lcp_exception_return_unexpected_server_error - ) - } - - /** - * Errors while parsing the License or Status JSON Documents. - */ - public sealed class Parsing( - @StringRes userMessageId: Int = R.string.readium_lcp_exception_parsing - ) : LcpException(userMessageId) { - - /** The JSON is malformed and can't be parsed. */ - public object MalformedJSON : Parsing(R.string.readium_lcp_exception_parsing_malformed_json) - - /** The JSON is not representing a valid License Document. */ - public object LicenseDocument : Parsing( - R.string.readium_lcp_exception_parsing_license_document - ) - - /** The JSON is not representing a valid Status Document. */ - public object StatusDocument : Parsing( - R.string.readium_lcp_exception_parsing_status_document - ) - - /** Invalid Link. */ - public object Link : Parsing() - - /** Invalid Encryption. */ - public object Encryption : Parsing() - - /** Invalid License Document Signature. */ - public object Signature : Parsing() - - /** Invalid URL for link with [rel]. */ - public class Url(public val rel: String) : Parsing() - } - - /** - * Errors while reading or writing a LCP container (LCPL, EPUB, LCPDF, etc.) - */ - public sealed class Container(@StringRes userMessageId: Int) : LcpException(userMessageId) { - - /** Can't access the container, it's format is wrong. */ - 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 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 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 url: Url?) : Container( - R.string.readium_lcp_exception_container_write_failed - ) - } - - /** - * An error occurred while checking the integrity of the License, it can't be retrieved. - */ - public sealed class LicenseIntegrity(@StringRes userMessageId: Int) : LcpException( - userMessageId - ) { - - public object CertificateRevoked : LicenseIntegrity( - R.string.readium_lcp_exception_license_integrity_certificate_revoked - ) - - public object InvalidCertificateSignature : LicenseIntegrity( - R.string.readium_lcp_exception_license_integrity_invalid_certificate_signature - ) - - public object InvalidLicenseSignatureDate : LicenseIntegrity( - R.string.readium_lcp_exception_license_integrity_invalid_license_signature_date - ) - - public object InvalidLicenseSignature : LicenseIntegrity( - R.string.readium_lcp_exception_license_integrity_invalid_license_signature - ) - - public object InvalidUserKeyCheck : LicenseIntegrity( - R.string.readium_lcp_exception_license_integrity_invalid_user_key_check - ) - } - - public sealed class Decryption(@StringRes userMessageId: Int) : LcpException(userMessageId) { - - public object ContentKeyDecryptError : Decryption( - R.string.readium_lcp_exception_decryption_content_key_decrypt_error - ) - - public object ContentDecryptError : Decryption( - R.string.readium_lcp_exception_decryption_content_decrypt_error - ) - } - - public companion object { - - internal fun wrap(e: Exception?): LcpException = when (e) { - is LcpException -> e - is SocketTimeoutException -> Network(e) - else -> Unknown(e) - } - } -} - -@Deprecated( - "Renamed to `LcpException`", - replaceWith = ReplaceWith("LcpException"), - level = DeprecationLevel.ERROR -) -public typealias LCPError = LcpException - -@Deprecated( - "Use `getUserMessage()` instead", - replaceWith = ReplaceWith("getUserMessage(context)"), - level = DeprecationLevel.ERROR -) -public val LcpException.errorDescription: String? get() = message diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt index 30b0552387..03ab20ec71 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt @@ -20,6 +20,7 @@ import org.readium.r2.shared.publication.services.ContentProtectionService import org.readium.r2.shared.util.Closeable import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.e import timber.log.Timber /** @@ -66,7 +67,7 @@ public interface LcpLicense : ContentProtectionService.UserRights, Closeable { * @param prefersWebPage Indicates whether the loan should be renewed through a web page if * available, instead of programmatically. */ - public suspend fun renewLoan(listener: RenewListener, prefersWebPage: Boolean = false): Try + public suspend fun renewLoan(listener: RenewListener, prefersWebPage: Boolean = false): Try /** * Can the user return the loaned publication? @@ -76,12 +77,12 @@ public interface LcpLicense : ContentProtectionService.UserRights, Closeable { /** * Returns the publication to its provider. */ - public suspend fun returnPublication(): Try + public suspend fun returnPublication(): Try /** * Decrypts the given [data] encrypted with the license's content key. */ - public suspend fun decrypt(data: ByteArray): Try + public suspend fun decrypt(data: ByteArray): Try /** * UX delegate for the loan renew LSD interaction. @@ -131,7 +132,7 @@ public interface LcpLicense : ContentProtectionService.UserRights, Closeable { ReplaceWith("renewLoan(LcpLicense.RenewListener)"), level = DeprecationLevel.ERROR ) - public suspend fun renewLoan(end: DateTime?, urlPresenter: suspend (URL) -> Unit): Try = Try.success( + public suspend fun renewLoan(end: DateTime?, urlPresenter: suspend (URL) -> Unit): Try = Try.success( Unit ) @@ -143,7 +144,7 @@ public interface LcpLicense : ContentProtectionService.UserRights, Closeable { public fun renewLoan( end: DateTime?, present: (URL, dismissed: () -> Unit) -> Unit, - completion: (LcpException?) -> Unit + completion: (LcpError?) -> Unit ) {} @Deprecated( @@ -152,7 +153,7 @@ public interface LcpLicense : ContentProtectionService.UserRights, Closeable { level = DeprecationLevel.ERROR ) @DelicateCoroutinesApi - public fun returnPublication(completion: (LcpException?) -> Unit) { + public fun returnPublication(completion: (LcpError?) -> Unit) { GlobalScope.launch { completion(returnPublication().failureOrNull()) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt index d09f420d82..6695d0c21d 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt @@ -57,7 +57,7 @@ public class LcpPublicationRetriever( */ public fun onAcquisitionFailed( requestId: RequestId, - error: LcpException + error: LcpError ) /** @@ -184,7 +184,7 @@ public class LcpPublicationRetriever( listenersForId.forEach { it.onAcquisitionFailed( lcpRequestId, - LcpException.wrap( + LcpError.wrap( Exception("Couldn't retrieve license from local storage.") ) ) @@ -209,7 +209,7 @@ public class LcpPublicationRetriever( } catch (e: Exception) { tryOrLog { download.file.delete() } listenersForId.forEach { - it.onAcquisitionFailed(lcpRequestId, LcpException.wrap(e)) + it.onAcquisitionFailed(lcpRequestId, LcpError.wrap(e)) } return@launch } @@ -257,7 +257,7 @@ public class LcpPublicationRetriever( listenersForId.forEach { it.onAcquisitionFailed( lcpRequestId, - LcpException.Network(ErrorException(error)) + LcpError.Network(ErrorException(error)) ) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt index 652b493063..7cd9932196 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt @@ -61,7 +61,7 @@ public interface LcpService { ReplaceWith("publicationRetriever()"), level = DeprecationLevel.ERROR ) - public suspend fun acquirePublication(lcpl: ByteArray, onProgress: (Double) -> Unit = {}): Try + public suspend fun acquirePublication(lcpl: ByteArray, onProgress: (Double) -> Unit = {}): Try /** * Acquires a protected publication from a standalone LCPL file. @@ -75,7 +75,7 @@ public interface LcpService { ReplaceWith("publicationRetriever()"), level = DeprecationLevel.ERROR ) - public suspend fun acquirePublication(lcpl: File, onProgress: (Double) -> Unit = {}): Try = withContext( + public suspend fun acquirePublication(lcpl: File, onProgress: (Double) -> Unit = {}): Try = withContext( Dispatchers.IO ) { throw NotImplementedError() @@ -95,7 +95,7 @@ public interface LcpService { mediaType: MediaType, authentication: LcpAuthenticating, allowUserInteraction: Boolean - ): Try + ): Try /** * Opens the LCP license of a protected publication, to access its DRM metadata and decipher @@ -115,7 +115,7 @@ public interface LcpService { asset: Asset, authentication: LcpAuthenticating, allowUserInteraction: Boolean - ): Try + ): Try /** * Creates an [LcpPublicationRetriever] instance which can be used to acquire a protected @@ -215,7 +215,7 @@ public interface LcpService { public fun importPublication( lcpl: ByteArray, authentication: LcpAuthenticating?, - completion: (AcquiredPublication?, LcpException?) -> Unit + completion: (AcquiredPublication?, LcpError?) -> Unit ) { throw NotImplementedError() } @@ -231,7 +231,7 @@ public interface LcpService { public fun retrieveLicense( publication: String, authentication: LcpAuthenticating?, - completion: (LcpLicense?, LcpException?) -> Unit + completion: (LcpLicense?, LcpError?) -> Unit ) { throw NotImplementedError() } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt index a33cf1918b..efcc9a363c 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/License.kt @@ -10,7 +10,7 @@ package org.readium.r2.lcp.license import java.net.HttpURLConnection -import java.util.* +import java.util.Date import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import org.readium.r2.lcp.BuildConfig.DEBUG +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.LcpLicense import org.readium.r2.lcp.license.model.LicenseDocument @@ -85,7 +86,7 @@ internal class License private constructor( override val status: StatusDocument? get() = documents.status - override suspend fun decrypt(data: ByteArray): Try = withContext( + override suspend fun decrypt(data: ByteArray): Try = withContext( Dispatchers.Default ) { try { @@ -98,7 +99,7 @@ internal class License private constructor( Try.success(decryptedData) } } catch (e: Exception) { - Try.failure(LcpException.wrap(e)) + Try.failure(LcpError.wrap(e)) } } @@ -146,7 +147,7 @@ internal class License private constructor( override val maxRenewDate: Date? get() = status?.potentialRights?.end - override suspend fun renewLoan(listener: LcpLicense.RenewListener, prefersWebPage: Boolean): Try { + override suspend fun renewLoan(listener: LcpLicense.RenewListener, prefersWebPage: Boolean): Try { // Finds the renew link according to `prefersWebPage`. fun findRenewLink(): Link? { val status = documents.status ?: return null @@ -186,11 +187,16 @@ internal class License private constructor( return network.fetch(url.toString(), NetworkService.Method.PUT) .getOrElse { error -> when (error.status) { - HttpURLConnection.HTTP_BAD_REQUEST -> throw LcpException.Renew.RenewFailed - HttpURLConnection.HTTP_FORBIDDEN -> throw LcpException.Renew.InvalidRenewalPeriod( - maxRenewDate = this.maxRenewDate - ) - else -> throw LcpException.Renew.UnexpectedServerError + HttpURLConnection.HTTP_BAD_REQUEST -> + throw LcpException(LcpError.Renew.RenewFailed) + HttpURLConnection.HTTP_FORBIDDEN -> + throw LcpException( + LcpError.Renew.InvalidRenewalPeriod( + maxRenewDate = this.maxRenewDate + ) + ) + else -> + throw LcpException(LcpError.Renew.UnexpectedServerError) } } } @@ -205,7 +211,7 @@ internal class License private constructor( LicenseDocument.Rel.Status, preferredType = MediaType.LCP_STATUS_DOCUMENT ) - } ?: throw LcpException.LicenseInteractionNotAvailable + } ?: throw LcpException(LcpError.LicenseInteractionNotAvailable) return network.fetch( statusURL.toString(), @@ -215,7 +221,7 @@ internal class License private constructor( try { val link = findRenewLink() - ?: throw LcpException.LicenseInteractionNotAvailable + ?: throw LcpException(LcpError.LicenseInteractionNotAvailable) val data = if (link.mediaType?.isHtml == true) { @@ -231,14 +237,14 @@ internal class License private constructor( // Passthrough for cancelled coroutines throw e } catch (e: Exception) { - return Try.failure(LcpException.wrap(e)) + return Try.failure(LcpError.wrap(e)) } } override val canReturnPublication: Boolean get() = status?.link(StatusDocument.Rel.Return) != null - override suspend fun returnPublication(): Try { + override suspend fun returnPublication(): Try { try { val status = this.documents.status val url = try { @@ -251,22 +257,26 @@ internal class License private constructor( null } if (status == null || url == null) { - throw LcpException.LicenseInteractionNotAvailable + throw LcpException(LcpError.LicenseInteractionNotAvailable) } network.fetch(url.toString(), method = NetworkService.Method.PUT) .onSuccess { validateStatusDocument(it) } .onFailure { error -> when (error.status) { - HttpURLConnection.HTTP_BAD_REQUEST -> throw LcpException.Return.ReturnFailed - HttpURLConnection.HTTP_FORBIDDEN -> throw LcpException.Return.AlreadyReturnedOrExpired - else -> throw LcpException.Return.UnexpectedServerError + HttpURLConnection.HTTP_BAD_REQUEST -> throw LcpException( + LcpError.Return.ReturnFailed + ) + HttpURLConnection.HTTP_FORBIDDEN -> throw LcpException( + LcpError.Return.AlreadyReturnedOrExpired + ) + else -> throw LcpException(LcpError.Return.UnexpectedServerError) } } return Try.success(Unit) } catch (e: Exception) { - return Try.failure(LcpException.wrap(e)) + return Try.failure(LcpError.wrap(e)) } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/LicenseValidation.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/LicenseValidation.kt index 9f9ec452a7..8c9d60435a 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/LicenseValidation.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/LicenseValidation.kt @@ -14,6 +14,7 @@ import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.runBlocking import org.readium.r2.lcp.BuildConfig.DEBUG import org.readium.r2.lcp.LcpAuthenticating +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.lcp.license.model.StatusDocument @@ -37,7 +38,7 @@ private val supportedProfiles = listOf( "http://readium.org/lcp/profile-1.0" ) -internal typealias Context = Either +internal typealias Context = Either internal typealias Observer = (ValidatedDocuments?, Exception?) -> Unit @@ -56,7 +57,7 @@ internal data class ValidatedDocuments constructor( fun getContext(): LcpClient.Context { when (context) { is Either.Left -> return context.left - is Either.Right -> throw context.right + is Either.Right -> throw LcpException(context.right) } } } @@ -89,7 +90,7 @@ internal sealed class Event { data class validatedLicense(val license: LicenseDocument) : Event() data class retrievedStatusData(val data: ByteArray) : Event() data class validatedStatus(val status: StatusDocument) : Event() - data class checkedLicenseStatus(val error: LcpException.LicenseStatus?) : Event() + data class checkedLicenseStatus(val error: LcpError.LicenseStatus?) : Event() data class retrievedPassphrase(val passphrase: String) : Event() data class validatedIntegrity(val context: LcpClient.Context) : Event() data class registeredDevice(val statusData: ByteArray?) : Event() @@ -98,7 +99,7 @@ internal sealed class Event { } /** - * If [ignoreInternetErrors] is true, then the validation won't fail on [LcpException.Network] errors. + * If [ignoreInternetErrors] is true, then the validation won't fail on [LcpError.Network] errors. * This should be the case with writable licenses (such as local ones) but not with read-only licences. */ internal class LicenseValidation( @@ -172,7 +173,7 @@ internal class LicenseValidation( transitionTo(State.validateStatus(license, it.data)) } on { - if (!ignoreInternetErrors && it.error is LcpException.Network) { + if (!ignoreInternetErrors && it.error is LcpException && it.error.error is LcpError.Network) { if (DEBUG) Timber.d("State.failure(it.error)") transitionTo(State.failure(it.error)) } else { @@ -341,7 +342,7 @@ internal class LicenseValidation( private fun validateLicense(data: ByteArray) { val license = LicenseDocument(data = data) if (!isProduction && license.encryption.profile != "http://readium.org/lcp/basic-profile") { - throw LcpException.LicenseProfileNotSupported + throw LcpException(LcpError.LicenseProfileNotSupported) } onLicenseValidated(license) raise(Event.validatedLicense(license)) @@ -359,7 +360,7 @@ internal class LicenseValidation( timeout = timeout, headers = mapOf("Accept" to MediaType.LCP_STATUS_DOCUMENT.toString()) ) - .getOrElse { throw LcpException.Network(it) } + .getOrElse { throw LcpException(LcpError.Network(it)) } raise(Event.retrievedStatusData(data)) } @@ -376,7 +377,7 @@ internal class LicenseValidation( ).toString() // Short timeout to avoid blocking the License, since it can be updated next time. val data = network.fetch(url, timeout = 5.seconds) - .getOrElse { throw LcpException.Network(it) } + .getOrElse { throw LcpException(LcpError.Network(it)) } raise(Event.retrievedLicenseData(data)) } @@ -386,7 +387,7 @@ internal class LicenseValidation( status: StatusDocument?, statusDocumentTakesPrecedence: Boolean ) { - var error: LcpException.LicenseStatus? = null + var error: LcpError.LicenseStatus? = null val now = Date() val start = license.rights.start ?: now val end = license.rights.end ?: now @@ -406,24 +407,24 @@ internal class LicenseValidation( when (status.status) { StatusDocument.Status.Ready, StatusDocument.Status.Active, StatusDocument.Status.Expired -> if (start > now) { - LcpException.LicenseStatus.NotStarted(start) + LcpError.LicenseStatus.NotStarted(start) } else { - LcpException.LicenseStatus.Expired(end) + LcpError.LicenseStatus.Expired(end) } - StatusDocument.Status.Returned -> LcpException.LicenseStatus.Returned(date) + StatusDocument.Status.Returned -> LcpError.LicenseStatus.Returned(date) StatusDocument.Status.Revoked -> { val devicesCount = status.events( org.readium.r2.lcp.license.model.components.lsd.Event.EventType.Register ).size - LcpException.LicenseStatus.Revoked(date, devicesCount = devicesCount) + LcpError.LicenseStatus.Revoked(date, devicesCount = devicesCount) } - StatusDocument.Status.Cancelled -> LcpException.LicenseStatus.Cancelled(date) + StatusDocument.Status.Cancelled -> LcpError.LicenseStatus.Cancelled(date) } } else { if (start > now) { - LcpException.LicenseStatus.NotStarted(start) + LcpError.LicenseStatus.NotStarted(start) } else { - LcpException.LicenseStatus.Expired(end) + LcpError.LicenseStatus.Expired(end) } } } @@ -444,7 +445,7 @@ internal class LicenseValidation( if (DEBUG) Timber.d("validateIntegrity") val profile = license.encryption.profile if (!supportedProfiles.contains(profile)) { - throw LcpException.LicenseProfileNotSupported + throw LcpException(LcpError.LicenseProfileNotSupported) } val context = LcpClient.createContext(license.json.toString(), passphrase, crl.retrieve()) raise(Event.validatedIntegrity(context)) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt index 90c7c018ba..21889e7fe8 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContainerLicenseContainer.kt @@ -7,6 +7,7 @@ package org.readium.r2.lcp.license.container import kotlinx.coroutines.runBlocking +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container @@ -24,11 +25,11 @@ internal class ContainerLicenseContainer( override fun read(): ByteArray { return runBlocking { val resource = container.get(entryUrl) - ?: throw LcpException.Container.FileNotFound(entryUrl) + ?: throw LcpException(LcpError.Container.FileNotFound(entryUrl)) resource.read() .mapFailure { - LcpException.Container.ReadFailed(entryUrl) + LcpException(LcpError.Container.ReadFailed(entryUrl)) } .getOrThrow() } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt index 566faae8c6..1816221f32 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt @@ -14,6 +14,7 @@ import java.io.File import java.io.FileOutputStream import java.util.UUID import java.util.zip.ZipFile +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.util.Url @@ -41,11 +42,11 @@ internal class ContentZipLicenseContainer( val tmpZip = File(cache, UUID.randomUUID().toString()) contentResolver.openInputStream(zipUri) ?.use { it.copyTo(FileOutputStream(tmpZip)) } - ?: throw LcpException.Container.WriteFailed(pathInZip) + ?: throw LcpException(LcpError.Container.WriteFailed(pathInZip)) val tmpZipFile = ZipFile(tmpZip) val outStream = contentResolver.openOutputStream(zipUri, "wt") - ?: throw LcpException.Container.WriteFailed(pathInZip) + ?: throw LcpException(LcpError.Container.WriteFailed(pathInZip)) tmpZipFile.addOrReplaceEntry( pathInZip.toString(), ByteArrayInputStream(license.toByteArray()), @@ -56,7 +57,7 @@ internal class ContentZipLicenseContainer( tmpZipFile.close() tmpZip.delete() } catch (e: Exception) { - throw LcpException.Container.WriteFailed(pathInZip) + throw LcpException(LcpError.Container.WriteFailed(pathInZip)) } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/FileZipLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/FileZipLicenseContainer.kt index 66be5ec8e7..c30897b07c 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/FileZipLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/FileZipLicenseContainer.kt @@ -12,6 +12,7 @@ package org.readium.r2.lcp.license.container import java.io.ByteArrayInputStream import java.io.File import java.util.zip.ZipFile +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.util.Url @@ -28,18 +29,18 @@ internal class FileZipLicenseContainer( val archive = try { ZipFile(zip) } catch (e: Exception) { - throw LcpException.Container.OpenFailed + throw LcpException(LcpError.Container.OpenFailed) } val entry = try { archive.getEntry(pathInZIP.toString())!! } catch (e: Exception) { - throw LcpException.Container.FileNotFound(pathInZIP) + throw LcpException(LcpError.Container.FileNotFound(pathInZIP)) } return try { archive.getInputStream(entry).readBytes() } catch (e: Exception) { - throw LcpException.Container.ReadFailed(pathInZIP) + throw LcpException(LcpError.Container.ReadFailed(pathInZIP)) } } @@ -56,7 +57,7 @@ internal class FileZipLicenseContainer( zipFile.close() tmpZip.moveTo(source) } catch (e: Exception) { - throw LcpException.Container.WriteFailed(pathInZIP) + throw LcpException(LcpError.Container.WriteFailed(pathInZIP)) } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplLicenseContainer.kt index 74439c7601..8ba1fcd33f 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplLicenseContainer.kt @@ -10,6 +10,7 @@ package org.readium.r2.lcp.license.container import java.io.File +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.util.toUrl @@ -23,14 +24,14 @@ internal class LcplLicenseContainer(private val licenseFile: File) : WritableLic try { licenseFile.readBytes() } catch (e: Exception) { - throw LcpException.Container.OpenFailed + throw LcpException(LcpError.Container.OpenFailed) } override fun write(license: LicenseDocument) { try { licenseFile.writeBytes(license.toByteArray()) } catch (e: Exception) { - throw LcpException.Container.WriteFailed(licenseFile.toUrl()) + throw LcpException(LcpError.Container.WriteFailed(licenseFile.toUrl())) } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt index 442cd638e5..c1d3628f14 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt @@ -10,6 +10,7 @@ package org.readium.r2.lcp.license.container import kotlinx.coroutines.runBlocking +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.resource.Resource @@ -24,6 +25,6 @@ internal class LcplResourceLicenseContainer(private val resource: Resource) : Li try { runBlocking { resource.read().assertSuccess() } } catch (e: Exception) { - throw LcpException.Container.OpenFailed + throw LcpException(LcpError.Container.OpenFailed) } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt index ba14a22409..5efad0fa99 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt @@ -11,6 +11,7 @@ package org.readium.r2.lcp.license.container import android.content.Context import java.io.File +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.util.Url @@ -59,7 +60,7 @@ internal fun createLicenseContainer( mediaType: MediaType ): LicenseContainer { if (mediaType != MediaType.LCP_LICENSE_DOCUMENT) { - throw LcpException.Container.OpenFailed + throw LcpException(LcpError.Container.OpenFailed) } return when { diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/LicenseDocument.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/LicenseDocument.kt index eb898ded7f..2e584b2c13 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/LicenseDocument.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/LicenseDocument.kt @@ -12,6 +12,7 @@ package org.readium.r2.lcp.license.model import java.nio.charset.Charset import java.util.* import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.components.Link import org.readium.r2.lcp.license.model.components.Links @@ -23,23 +24,49 @@ import org.readium.r2.lcp.service.URLParameters import org.readium.r2.shared.extensions.iso8601ToDate import org.readium.r2.shared.extensions.optNullableString 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.mediatype.MediaType public class LicenseDocument internal constructor(public val json: JSONObject) { + public companion object { + + public fun fromJSON(json: JSONObject): Try { + val document = try { + LicenseDocument(json) + } catch (e: Exception) { + check(e is LcpException) + check(e.error is LcpError.Parsing) + return Try.failure(e.error) + } + + return Try.success(document) + } + + public fun fromBytes(data: ByteArray): Try { + val json = try { + JSONObject(data.decodeToString()) + } catch (e: Exception) { + return Try.failure(LcpError.Parsing.MalformedJSON) + } + + return fromJSON(json) + } + } + public val provider: String = json.optNullableString("provider") - ?: throw LcpException.Parsing.LicenseDocument + ?: throw LcpException(LcpError.Parsing.LicenseDocument) public val id: String = json.optNullableString("id") - ?: throw LcpException.Parsing.LicenseDocument + ?: throw LcpException(LcpError.Parsing.LicenseDocument) public val issued: Date = json.optNullableString("issued") ?.iso8601ToDate() - ?: throw LcpException.Parsing.LicenseDocument + ?: throw LcpException(LcpError.Parsing.LicenseDocument) public val updated: Date = json.optNullableString("updated") @@ -49,12 +76,12 @@ public class LicenseDocument internal constructor(public val json: JSONObject) { public val encryption: Encryption = json.optJSONObject("encryption") ?.let { Encryption(it) } - ?: throw LcpException.Parsing.LicenseDocument + ?: throw LcpException(LcpError.Parsing.LicenseDocument) public val links: Links = json.optJSONArray("links") ?.let { Links(it) } - ?: throw LcpException.Parsing.LicenseDocument + ?: throw LcpException(LcpError.Parsing.LicenseDocument) public val user: User = User(json.optJSONObject("user") ?: JSONObject()) @@ -65,26 +92,26 @@ public class LicenseDocument internal constructor(public val json: JSONObject) { public val signature: Signature = json.optJSONObject("signature") ?.let { Signature(it) } - ?: throw LcpException.Parsing.LicenseDocument + ?: throw LcpException(LcpError.Parsing.LicenseDocument) init { if (link(Rel.Hint) == null || link(Rel.Publication) == null) { - throw LcpException.Parsing.LicenseDocument + throw LcpException(LcpError.Parsing.LicenseDocument) } // Check that the acquisition link has a valid URL. try { link(Rel.Publication)!!.url() as AbsoluteUrl } catch (e: Exception) { - throw LcpException.Parsing.Url(rel = LicenseDocument.Rel.Publication.value) + throw LcpException(LcpError.Parsing.Url(rel = LicenseDocument.Rel.Publication.value)) } } - public constructor(data: ByteArray) : this( + internal constructor(data: ByteArray) : this( try { JSONObject(data.decodeToString()) } catch (e: Exception) { - throw LcpException.Parsing.MalformedJSON + throw LcpException(LcpError.Parsing.MalformedJSON) } ) @@ -119,7 +146,7 @@ public class LicenseDocument internal constructor(public val json: JSONObject) { ): Url { val link = link(rel, preferredType) ?: links.firstWithRelAndNoType(rel.value) - ?: throw LcpException.Parsing.Url(rel = rel.value) + ?: throw LcpException(LcpError.Parsing.Url(rel = rel.value)) return link.url(parameters = parameters) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/StatusDocument.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/StatusDocument.kt index 1ee80e8d94..aea56dd363 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/StatusDocument.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/StatusDocument.kt @@ -12,6 +12,7 @@ package org.readium.r2.lcp.license.model import java.nio.charset.Charset import java.util.* import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.components.Link import org.readium.r2.lcp.license.model.components.Links @@ -70,18 +71,28 @@ public class StatusDocument(public val data: ByteArray) { try { json = JSONObject(data.toString(Charset.defaultCharset())) } catch (e: Exception) { - throw LcpException.Parsing.MalformedJSON + throw LcpException(LcpError.Parsing.MalformedJSON) } - id = json.optNullableString("id") ?: throw LcpException.Parsing.StatusDocument - status = json.optNullableString("status")?.let { Status(it) } ?: throw LcpException.Parsing.StatusDocument - message = json.optNullableString("message") ?: throw LcpException.Parsing.StatusDocument + id = json.optNullableString("id") ?: throw LcpException(LcpError.Parsing.StatusDocument) + status = json.optNullableString("status")?.let { Status(it) } ?: throw LcpException( + LcpError.Parsing.StatusDocument + ) + message = json.optNullableString("message") ?: throw LcpException( + LcpError.Parsing.StatusDocument + ) val updated = json.optJSONObject("updated") ?: JSONObject() - licenseUpdated = updated.optNullableString("license")?.iso8601ToDate() ?: throw LcpException.Parsing.StatusDocument - statusUpdated = updated.optNullableString("status")?.iso8601ToDate() ?: throw LcpException.Parsing.StatusDocument + licenseUpdated = updated.optNullableString("license")?.iso8601ToDate() ?: throw LcpException( + LcpError.Parsing.StatusDocument + ) + statusUpdated = updated.optNullableString("status")?.iso8601ToDate() ?: throw LcpException( + LcpError.Parsing.StatusDocument + ) - links = json.optJSONArray("links")?.let { Links(it) } ?: throw LcpException.Parsing.StatusDocument + links = json.optJSONArray("links")?.let { Links(it) } ?: throw LcpException( + LcpError.Parsing.StatusDocument + ) potentialRights = json.optJSONObject("potential_rights")?.let { PotentialRights(it) } @@ -108,7 +119,7 @@ public class StatusDocument(public val data: ByteArray) { ): Url { val link = link(rel, preferredType) ?: linkWithNoType(rel) - ?: throw LcpException.Parsing.Url(rel = rel.value) + ?: throw LcpException(LcpError.Parsing.Url(rel = rel.value)) return link.url(parameters = parameters) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt index cdeafd2671..cb0cbe62a3 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt @@ -11,6 +11,7 @@ package org.readium.r2.lcp.license.model.components import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.extensions.optNullableInt import org.readium.r2.shared.extensions.optNullableString @@ -42,7 +43,7 @@ public data class Link( templated = json.optBoolean("templated", false) ) } - ?: throw LcpException.Parsing.Link + ?: throw LcpException(LcpError.Parsing.Link) return Link( href = href, @@ -51,7 +52,7 @@ public data class Link( title = json.optNullableString("title"), rels = json.optStringsFromArrayOrSingle("rel").toSet() .takeIf { it.isNotEmpty() } - ?: throw LcpException.Parsing.Link, + ?: throw LcpException(LcpError.Parsing.Link), profile = json.optNullableString("profile"), length = json.optNullableInt("length"), hash = json.optNullableString("hash") diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/ContentKey.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/ContentKey.kt index cc74da1fcb..56e5e6a50b 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/ContentKey.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/ContentKey.kt @@ -10,6 +10,7 @@ package org.readium.r2.lcp.license.model.components.lcp import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException public data class ContentKey(val json: JSONObject) { @@ -17,7 +18,19 @@ public data class ContentKey(val json: JSONObject) { val encryptedValue: String init { - algorithm = if (json.has("algorithm")) json.getString("algorithm") else throw LcpException.Parsing.Encryption - encryptedValue = if (json.has("encrypted_value")) json.getString("encrypted_value") else throw LcpException.Parsing.Encryption + algorithm = if (json.has("algorithm")) { + json.getString("algorithm") + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } + encryptedValue = if (json.has("encrypted_value")) { + json.getString("encrypted_value") + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Encryption.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Encryption.kt index 90ed33d836..0899a3f8fa 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Encryption.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Encryption.kt @@ -10,6 +10,7 @@ package org.readium.r2.lcp.license.model.components.lcp import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException public data class Encryption(val json: JSONObject) { @@ -18,8 +19,26 @@ public data class Encryption(val json: JSONObject) { val userKey: UserKey init { - profile = if (json.has("profile")) json.getString("profile") else throw LcpException.Parsing.Encryption - contentKey = if (json.has("content_key")) ContentKey(json.getJSONObject("content_key")) else throw LcpException.Parsing.Encryption - userKey = if (json.has("user_key")) UserKey(json.getJSONObject("user_key")) else throw LcpException.Parsing.Encryption + profile = if (json.has("profile")) { + json.getString("profile") + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } + contentKey = if (json.has("content_key")) { + ContentKey(json.getJSONObject("content_key")) + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } + userKey = if (json.has("user_key")) { + UserKey(json.getJSONObject("user_key")) + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Signature.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Signature.kt index 0e28d48b77..e17429c077 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Signature.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/Signature.kt @@ -10,11 +10,18 @@ package org.readium.r2.lcp.license.model.components.lcp import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.extensions.optNullableString public data class Signature(val json: JSONObject) { - val algorithm: String = json.optNullableString("algorithm") ?: throw LcpException.Parsing.Signature - val certificate: String = json.optNullableString("certificate") ?: throw LcpException.Parsing.Signature - val value: String = json.optNullableString("value") ?: throw LcpException.Parsing.Signature + val algorithm: String = json.optNullableString("algorithm") ?: throw LcpException( + LcpError.Parsing.Signature + ) + val certificate: String = json.optNullableString("certificate") ?: throw LcpException( + LcpError.Parsing.Signature + ) + val value: String = json.optNullableString("value") ?: throw LcpException( + LcpError.Parsing.Signature + ) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/UserKey.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/UserKey.kt index f61f2a4bf6..9b624d86b1 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/UserKey.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/lcp/UserKey.kt @@ -10,6 +10,7 @@ package org.readium.r2.lcp.license.model.components.lcp import org.json.JSONObject +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException public data class UserKey(val json: JSONObject) { @@ -18,8 +19,26 @@ public data class UserKey(val json: JSONObject) { val keyCheck: String init { - textHint = if (json.has("text_hint")) json.getString("text_hint") else throw LcpException.Parsing.Encryption - algorithm = if (json.has("algorithm")) json.getString("algorithm") else throw LcpException.Parsing.Encryption - keyCheck = if (json.has("key_check")) json.getString("key_check") else throw LcpException.Parsing.Encryption + textHint = if (json.has("text_hint")) { + json.getString("text_hint") + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } + algorithm = if (json.has("algorithm")) { + json.getString("algorithm") + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } + keyCheck = if (json.has("key_check")) { + json.getString("key_check") + } else { + throw LcpException( + LcpError.Parsing.Encryption + ) + } } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/public/Deprecated.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/public/Deprecated.kt index b76f1bf63b..ac5585d8c9 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/public/Deprecated.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/public/Deprecated.kt @@ -11,7 +11,7 @@ package org.readium.r2.lcp.public import android.content.Context import org.readium.r2.lcp.LcpAuthenticating -import org.readium.r2.lcp.LcpException +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpLicense import org.readium.r2.lcp.LcpService @@ -68,7 +68,7 @@ public typealias LCPAuthenticatedLicense = LcpAuthenticating.AuthenticatedLicens ReplaceWith("org.readium.r2.lcp.LcpException"), level = DeprecationLevel.ERROR ) -public typealias LCPError = LcpException +public typealias LCPError = LcpError @Deprecated( "Renamed to `LcpService()`", diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/CRLService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/CRLService.kt index c183fe4bcc..c896fbc666 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/CRLService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/CRLService.kt @@ -17,6 +17,7 @@ import kotlin.time.ExperimentalTime import org.joda.time.DateTime import org.joda.time.Days import org.readium.r2.lcp.BuildConfig.DEBUG +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.getOrElse import timber.log.Timber @@ -53,7 +54,7 @@ internal class CRLService(val network: NetworkService, val context: Context) { private suspend fun fetch(): String { val url = "http://crl.edrlab.telesec.de/rl/EDRLab_CA.crl" val data = network.fetch(url, NetworkService.Method.GET) - .getOrElse { throw LcpException.CrlFetching } + .getOrElse { throw LcpException(LcpError.CrlFetching) } return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { "-----BEGIN X509 CRL-----${Base64.getEncoder().encodeToString(data)}-----END X509 CRL-----" diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/DeviceRepository.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/DeviceRepository.kt index 9ded1bcfa5..dfd7f64209 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/DeviceRepository.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/DeviceRepository.kt @@ -9,6 +9,7 @@ package org.readium.r2.lcp.service +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.lcp.persistence.LcpDao @@ -17,14 +18,14 @@ internal class DeviceRepository(private val lcpDao: LcpDao) { suspend fun isDeviceRegistered(license: LicenseDocument): Boolean { if (lcpDao.exists(license.id) == null) { - throw LcpException.Runtime("The LCP License doesn't exist in the database") + throw LcpException(LcpError.Runtime("The LCP License doesn't exist in the database")) } return lcpDao.isDeviceRegistered(license.id) } suspend fun registerDevice(license: LicenseDocument) { if (lcpDao.exists(license.id) == null) { - throw LcpException.Runtime("The LCP License doesn't exist in the database") + throw LcpException(LcpError.Runtime("The LCP License doesn't exist in the database")) } lcpDao.registerDevice(license.id) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LcpClient.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LcpClient.kt index 2a544d9fb0..5be0dd1dd7 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LcpClient.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LcpClient.kt @@ -1,6 +1,7 @@ package org.readium.r2.lcp.service import java.lang.reflect.InvocationTargetException +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.extensions.tryOr @@ -91,7 +92,7 @@ internal object LcpClient { val drmExceptionClass = Class.forName("org.readium.lcp.sdk.DRMException") if (!drmExceptionClass.isInstance(e)) { - return LcpException.Runtime("the Lcp client threw an unhandled exception") + return LcpException(LcpError.Runtime("the Lcp client threw an unhandled exception")) } val drmError = drmExceptionClass @@ -103,19 +104,21 @@ internal object LcpClient { .getMethod("getCode") .invoke(drmError) as Int - return when (errorCode) { + val error = when (errorCode) { // Error code 11 should never occur since we check the start/end date before calling createContext - 11 -> LcpException.Runtime("License is out of date (check start and end date).") - 101 -> LcpException.LicenseIntegrity.CertificateRevoked - 102 -> LcpException.LicenseIntegrity.InvalidCertificateSignature - 111 -> LcpException.LicenseIntegrity.InvalidLicenseSignatureDate - 112 -> LcpException.LicenseIntegrity.InvalidLicenseSignature + 11 -> LcpError.Runtime("License is out of date (check start and end date).") + 101 -> LcpError.LicenseIntegrity.CertificateRevoked + 102 -> LcpError.LicenseIntegrity.InvalidCertificateSignature + 111 -> LcpError.LicenseIntegrity.InvalidLicenseSignatureDate + 112 -> LcpError.LicenseIntegrity.InvalidLicenseSignature // Error code 121 seems to be unused in the C++ lib. - 121 -> LcpException.Runtime("The drm context is invalid.") - 131 -> LcpException.Decryption.ContentKeyDecryptError - 141 -> LcpException.LicenseIntegrity.InvalidUserKeyCheck - 151 -> LcpException.Decryption.ContentDecryptError - else -> LcpException.Unknown(e) + 121 -> LcpError.Runtime("The drm context is invalid.") + 131 -> LcpError.Decryption.ContentKeyDecryptError + 141 -> LcpError.LicenseIntegrity.InvalidUserKeyCheck + 151 -> LcpError.Decryption.ContentDecryptError + else -> LcpError.Unknown(e) } + + return LcpException(error) } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index 8ee6793982..f913117a97 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import org.readium.r2.lcp.LcpAuthenticating import org.readium.r2.lcp.LcpContentProtection -import org.readium.r2.lcp.LcpException +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpLicense import org.readium.r2.lcp.LcpPublicationRetriever import org.readium.r2.lcp.LcpService @@ -92,13 +92,13 @@ internal class LicensesService( ReplaceWith("publicationRetriever()"), level = DeprecationLevel.ERROR ) - override suspend fun acquirePublication(lcpl: ByteArray, onProgress: (Double) -> Unit): Try = + override suspend fun acquirePublication(lcpl: ByteArray, onProgress: (Double) -> Unit): Try = try { val licenseDocument = LicenseDocument(lcpl) Timber.d("license ${licenseDocument.json}") fetchPublication(licenseDocument, onProgress).let { Try.success(it) } } catch (e: Exception) { - Try.failure(LcpException.wrap(e)) + Try.failure(LcpError.wrap(e)) } override suspend fun retrieveLicense( @@ -106,7 +106,7 @@ internal class LicensesService( mediaType: MediaType, authentication: LcpAuthenticating, allowUserInteraction: Boolean - ): Try = + ): Try = try { val container = createLicenseContainer(file, mediaType) val license = retrieveLicense( @@ -116,14 +116,14 @@ internal class LicensesService( ) Try.success(license) } catch (e: Exception) { - Try.failure(LcpException.wrap(e)) + Try.failure(LcpError.wrap(e)) } override suspend fun retrieveLicense( asset: Asset, authentication: LcpAuthenticating, allowUserInteraction: Boolean - ): Try = + ): Try = try { val licenseContainer = createLicenseContainer(context, asset) val license = retrieveLicense( @@ -133,7 +133,7 @@ internal class LicensesService( ) Try.success(license) } catch (e: Exception) { - Try.failure(LcpException.wrap(e)) + Try.failure(LcpError.wrap(e)) } private suspend fun retrieveLicense( diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt index c365f6c9c1..76ea7188d0 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt @@ -19,6 +19,7 @@ import kotlin.math.round import kotlin.time.Duration import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url @@ -102,7 +103,7 @@ internal class NetworkService( try { val connection = URL(url.toString()).openConnection() as HttpURLConnection if (connection.responseCode >= 400) { - throw LcpException.Network(NetworkException(connection.responseCode)) + throw LcpException(LcpError.Network(NetworkException(connection.responseCode))) } var readLength = 0L @@ -143,7 +144,7 @@ internal class NetworkService( ) } catch (e: Exception) { Timber.e(e) - throw LcpException.Network(e) + throw LcpException(LcpError.Network(e)) } } } diff --git a/readium/lcp/src/main/res/values/strings.xml b/readium/lcp/src/main/res/values/strings.xml index e0b90acd93..7a2bab272c 100644 --- a/readium/lcp/src/main/res/values/strings.xml +++ b/readium/lcp/src/main/res/values/strings.xml @@ -21,49 +21,4 @@ Phone Mail - - - This interaction is not available - This License has a profile identifier that this app cannot handle, the publication cannot be processed - Can\'t retrieve the Certificate Revocation List - Network error - Unexpected LCP error - Unknown LCP error - - This license was cancelled on %1$s - This license has been returned on %1$s - This license starts on %1$s - This license expired on %1$s - - This license was revoked by its provider on %1$s. It was registered by %2$d device. - This license was revoked by its provider on %1$s. It was registered by %2$d devices. - - - Your publication could not be renewed properly - Incorrect renewal period, your publication could not be renewed - An unexpected error has occurred on the server - - Your publication could not be returned properly - Your publication has already been returned before or is expired - An unexpected error has occurred on the server - - The JSON is not representing a valid document - The JSON is malformed and can\'t be parsed - The JSON is not representing a valid License Document - The JSON is not representing a valid Status Document - - Can\'t open the license container - License not found in container - Can\'t read license from container - Can\'t write license in container - - Certificate has been revoked in the CRL - Certificate has not been signed by CA - License has been issued by an expired certificate - License signature does not match - User key check invalid - - Unable to decrypt encrypted content key from user key - Unable to decrypt encrypted content from content key - \ No newline at end of file diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngine.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngine.kt index 5b08cad548..25c6ecb281 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngine.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsEngine.kt @@ -9,6 +9,7 @@ package org.readium.navigator.media.tts import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.Closeable +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Language /** @@ -50,7 +51,7 @@ public interface TtsEngine, /** * Marker interface for the errors that the [TtsEngine] returns. */ - public interface Error + public interface Error : org.readium.r2.shared.util.Error /** * An id to identify a request to speak. diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt index 842b37dd2c..2ae114ed3f 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigator.kt @@ -79,12 +79,19 @@ public class TtsNavigator, public object Ended : MediaNavigator.State.Ended - public sealed class Error : MediaNavigator.State.Error { + public data class Error(val error: TtsNavigator.Error) : MediaNavigator.State.Error + } - public data class EngineError (val error: E) : Error() + public sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { - public data class ContentError(val error: org.readium.r2.shared.util.Error) : Error() - } + public class EngineError (override val cause: E) : + Error("An error occurred in the TTS engine.", cause) + + public class ContentError(cause: org.readium.r2.shared.util.Error) : + Error("An error occurred while trying to read publication content.", cause) } public val voices: Set get() = @@ -181,8 +188,8 @@ public class TtsNavigator, private fun TtsPlayer.State.Error.toError(): State.Error = when (this) { - is TtsPlayer.State.Error.ContentError -> State.Error.ContentError(error) - is TtsPlayer.State.Error.EngineError<*> -> State.Error.EngineError(error) + is TtsPlayer.State.Error.ContentError -> State.Error(Error.ContentError(error)) + is TtsPlayer.State.Error.EngineError<*> -> State.Error(Error.EngineError(error)) } private fun TtsPlayer.Utterance.toPosition(): Location { diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt index 4e97d3d2d0..5ed0ead3d9 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt @@ -104,20 +104,6 @@ public class TtsNavigatorFactory null } } + public sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { + + public class UnsupportedPublication( + cause: org.readium.r2.shared.util.Error? = null + ) : Error("Publication is not supported.", cause) + + public class EngineInitialization( + cause: org.readium.r2.shared.util.Error? = null + ) : Error("Failed to initialize TTS engine.", cause) + } + public suspend fun createNavigator( listener: TtsNavigator.Listener, initialLocator: Locator? = null, diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsPlayer.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsPlayer.kt index 2f41beaab9..3d56b5c122 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsPlayer.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsPlayer.kt @@ -28,7 +28,7 @@ import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Locator -import org.readium.r2.shared.util.Error as SharedError +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.ThrowableError import timber.log.Timber @@ -536,7 +536,7 @@ internal class TtsPlayer, onContentError(error) } - private fun onContentError(error: SharedError) { + private fun onContentError(error: Error) { playbackMutable.value = playbackMutable.value.copy( state = State.Error.ContentError(error) ) diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngine.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngine.kt index b406322130..ca6194aa5c 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngine.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/android/AndroidTtsEngine.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.asStateFlow import org.readium.navigator.media.tts.TtsEngine import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.extensions.tryOrNull +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Language /* @@ -141,31 +142,34 @@ public class AndroidTtsEngine private constructor( public fun voice(language: Language?, availableVoices: Set): Voice? } - public sealed class Error : TtsEngine.Error { + public sealed class Error( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? = null + ) : TtsEngine.Error { /** Denotes a generic operation failure. */ - public object Unknown : Error() + public object Unknown : Error("An unknown error occurred.") /** Denotes a failure caused by an invalid request. */ - public object InvalidRequest : Error() + public object InvalidRequest : Error("Invalid request") /** Denotes a failure caused by a network connectivity problems. */ - public object Network : Error() + public object Network : Error("A network error occurred.") /** Denotes a failure caused by network timeout. */ - public object NetworkTimeout : Error() + public object NetworkTimeout : Error("Network timeout") /** Denotes a failure caused by an unfinished download of the voice data. */ - public object NotInstalledYet : Error() + public object NotInstalledYet : Error("Voice not installed yet.") /** Denotes a failure related to the output (audio device or a file). */ - public object Output : Error() + public object Output : Error("An error related to the output occurred.") /** Denotes a failure of a TTS service. */ - public object Service : Error() + public object Service : Error("An error occurred with the TTS service.") /** Denotes a failure of a TTS engine to synthesize the given input. */ - public object Synthesis : Error() + public object Synthesis : Error("Synthesis failed.") /** * Denotes the language data is missing. @@ -173,7 +177,8 @@ public class AndroidTtsEngine private constructor( * You can open the Android settings to install the missing data with: * AndroidTtsEngine.requestInstallVoice(context) */ - public data class LanguageMissingData(val language: Language) : Error() + public data class LanguageMissingData(val language: Language) : + Error("Language data is missing.") /** * Android's TTS error code. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index 04a6bcc106..daa52c27e4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -9,21 +9,14 @@ package org.readium.r2.shared.publication.protection -import androidx.annotation.StringRes -import kotlin.Any import kotlin.Boolean import kotlin.Deprecated import kotlin.DeprecationLevel -import kotlin.Int import kotlin.String -import kotlin.Throwable import kotlin.Unit -import org.readium.r2.shared.R -import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.LocalizedString import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.ContentProtectionService -import org.readium.r2.shared.util.Error as BaseError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError @@ -41,15 +34,15 @@ public interface ContentProtection { public sealed class Error( override val message: String, - override val cause: BaseError? - ) : BaseError { + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { - public class AccessError( + public class ReadError( override val cause: org.readium.r2.shared.util.data.ReadError ) : Error("An error occurred while trying to read asset.", cause) public class UnsupportedAsset( - override val cause: BaseError? + override val cause: org.readium.r2.shared.util.Error? ) : Error("Asset is not supported.", cause) } @@ -108,30 +101,4 @@ public interface ContentProtection { public val Adept: Scheme = Scheme(uri = "http://ns.adobe.com/adept") } } - - public sealed class Exception( - userMessageId: Int, - vararg args: Any?, - quantity: Int? = null, - cause: Throwable? = null - ) : UserException(userMessageId, quantity, *args, cause = cause) { - protected constructor( - @StringRes userMessageId: Int, - vararg args: Any?, - cause: Throwable? = null - ) : this(userMessageId, *args, quantity = null, cause = cause) - - /** - * Exception returned when the given Content Protection [scheme] is not supported by the - * app. - */ - public class SchemeNotSupported(public val scheme: Scheme? = null, name: String?) : Exception( - if (name == null) { - R.string.readium_shared_publication_content_protection_exception_not_supported_unknown - } else { - R.string.readium_shared_publication_content_protection_exception_not_supported - }, - name - ) - } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt index 501a84a3ff..5df2c726db 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt @@ -9,7 +9,6 @@ package org.readium.r2.shared.publication.protection import kotlin.String import kotlin.let import kotlin.takeIf -import org.readium.r2.shared.util.Error as BaseError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse @@ -29,7 +28,7 @@ public class ContentProtectionSchemeRetriever( public sealed class Error( override val message: String, override val cause: org.readium.r2.shared.util.Error? - ) : BaseError { + ) : org.readium.r2.shared.util.Error { public object NoContentProtectionFound : Error("No content protection recognized the given asset.", null) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtectionService.kt index b03d3c8eda..1885eda641 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtectionService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtectionService.kt @@ -6,28 +6,36 @@ package org.readium.r2.shared.publication.protection -import org.readium.r2.shared.UserException -import org.readium.r2.shared.publication.LocalizedString import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.ContentProtectionService +import org.readium.r2.shared.util.Error internal class FallbackContentProtectionService( - override val scheme: ContentProtection.Scheme?, - override val name: LocalizedString? + override val scheme: ContentProtection.Scheme, + override val name: String ) : ContentProtectionService { override val isRestricted: Boolean = true override val credentials: String? = null override val rights = ContentProtectionService.UserRights.AllRestricted - override val error: UserException = - ContentProtection.Exception.SchemeNotSupported(scheme, name?.string) + override val error: Error = SchemeNotSupportedError(scheme, name) + + private class SchemeNotSupportedError( + val scheme: ContentProtection.Scheme, + val name: String + ) : Error { + + override val message: String = "$name DRM scheme is not supported." + + override val cause: Error? = null + } companion object { fun createFactory( - scheme: ContentProtection.Scheme?, - name: String? + scheme: ContentProtection.Scheme, + name: String ): (Publication.Service.Context) -> ContentProtectionService = - { FallbackContentProtectionService(scheme, name?.let { LocalizedString(it) }) } + { FallbackContentProtectionService(scheme, name) } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt index 2802aafc37..0325ff2466 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt @@ -11,8 +11,6 @@ package org.readium.r2.shared.publication.services import java.util.Locale import org.json.JSONObject -import org.readium.r2.shared.UserException -import org.readium.r2.shared.extensions.putIfNotEmpty import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.LocalizedString @@ -20,6 +18,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationServicesHolder import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.publication.protection.ContentProtection +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.http.HttpError @@ -43,7 +42,7 @@ public interface ContentProtectionService : Publication.WebService { /** * The error raised when trying to unlock the [Publication], if any. */ - public val error: UserException? + public val error: Error? /** * Credentials used to unlock this [Publication]. @@ -64,7 +63,7 @@ public interface ContentProtectionService : Publication.WebService { * User-facing name for this Content Protection, e.g. "Readium LCP". * It could be used in a sentence such as "Protected by {name}" */ - public val name: LocalizedString? get() = null + public val name: String? get() = null override val links: List get() = RouteHandler.links @@ -198,7 +197,7 @@ public val Publication.isRestricted: Boolean /** * The error raised when trying to unlock the [Publication], if any. */ -public val Publication.protectionError: UserException? +public val Publication.protectionError: Error? get() = protectionService?.error /** @@ -224,15 +223,21 @@ public val Publication.protectionScheme: ContentProtection.Scheme? * User-facing localized name for this Content Protection, e.g. "Readium LCP". * It could be used in a sentence such as "Protected by {name}". */ -public val Publication.protectionLocalizedName: LocalizedString? - get() = protectionService?.name +@Suppress("UnusedReceiverParameter") +@Deprecated( + "Localize protection names yourself.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("protectionName") +) +public val Publication.protectionLocalizedName: LocalizedString + get() = throw NotImplementedError() /** * User-facing name for this Content Protection, e.g. "Readium LCP". * It could be used in a sentence such as "Protected by {name}". */ public val Publication.protectionName: String? - get() = protectionLocalizedName?.string + get() = protectionService?.name private sealed class RouteHandler { @@ -271,8 +276,8 @@ private sealed class RouteHandler { override suspend fun handleRequest(request: HttpRequest, service: ContentProtectionService): Try { val json = JSONObject().apply { put("isRestricted", service.isRestricted) - putOpt("error", service.error?.localizedMessage) - putIfNotEmpty("name", service.name) + putOpt("error", service.error?.message) + putOpt("name", service.name) put("rights", service.rights.toJSON()) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 3040be7c2d..d25c72ede8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -10,7 +10,6 @@ import java.io.File import kotlin.Exception import kotlin.String import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.Error as SharedError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url @@ -18,6 +17,7 @@ import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.archive.ArchiveProvider import org.readium.r2.shared.util.archive.CompositeArchiveFactory import org.readium.r2.shared.util.archive.FileZipArchiveProvider +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.CompositeMediaTypeSniffer import org.readium.r2.shared.util.mediatype.MediaType @@ -42,26 +42,26 @@ public class AssetRetriever( public sealed class Error( override val message: String, - override val cause: SharedError? - ) : SharedError { + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { public class SchemeNotSupported( public val scheme: Url.Scheme, - cause: SharedError? = null + cause: org.readium.r2.shared.util.Error? = null ) : Error("Scheme $scheme is not supported.", cause) { public constructor(scheme: Url.Scheme, exception: Exception) : this(scheme, ThrowableError(exception)) } - public class ArchiveFormatNotSupported(cause: SharedError?) : + public class ArchiveFormatNotSupported(cause: org.readium.r2.shared.util.Error?) : Error("Archive factory does not support this kind of archive.", cause) { public constructor(exception: Exception) : this(ThrowableError(exception)) } - public class AccessError(override val cause: org.readium.r2.shared.util.data.ReadError) : + public class AccessError(override val cause: ReadError) : Error("An error occurred when trying to read asset.", cause) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt index 2a829d8bd8..824ac5f223 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt @@ -9,7 +9,6 @@ package org.readium.r2.shared.util.asset import kotlin.String import kotlin.let import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.Error as SharedError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType @@ -22,12 +21,12 @@ public interface ResourceFactory { public sealed class Error( override val message: String, - override val cause: SharedError? - ) : SharedError { + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { public class SchemeNotSupported( public val scheme: Url.Scheme, - cause: SharedError? = null + cause: org.readium.r2.shared.util.Error? = null ) : Error("Url scheme $scheme is not supported.", cause) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index 93d3956c83..ed9ac249f2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -38,7 +38,7 @@ public sealed class DecoderError( internal suspend fun Try.decode( block: (value: S) -> R, - wrapException: (Exception) -> Error + wrapError: (Exception) -> Error ): Try = when (this) { is Try.Success -> @@ -49,7 +49,7 @@ internal suspend fun Try.decode( } ) } catch (e: Exception) { - Try.failure(DecoderError.DecodingError(wrapException(e))) + Try.failure(DecoderError.DecodingError(wrapError(e))) } is Try.Failure -> Try.failure(DecoderError.DataAccess(value)) @@ -57,7 +57,7 @@ internal suspend fun Try.decode( internal suspend fun Try.decodeMap( block: (value: S) -> R, - wrapException: (Exception) -> Error + wrapError: (Exception) -> Error ): Try = when (this) { is Try.Success -> @@ -68,7 +68,7 @@ internal suspend fun Try.decodeMap( } ) } catch (e: Exception) { - Try.failure(DecoderError.DecodingError(wrapException(e))) + Try.failure(DecoderError.DecodingError(wrapError(e))) } is Try.Failure -> Try.failure(value) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index d7fae789f1..d8dc8faf2f 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -202,7 +202,7 @@ public class PublicationFactory( ?.open(asset, credentials, allowUserInteraction) ?.mapFailure { when (it) { - is ContentProtection.Error.AccessError -> + is ContentProtection.Error.ReadError -> Error.ReadError(it.cause) is ContentProtection.Error.UnsupportedAsset -> Error.UnsupportedAsset(it) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt index 02fe5a44c5..c2b1929b4f 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt @@ -9,7 +9,6 @@ package org.readium.r2.streamer.parser import kotlin.String import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationContainer -import org.readium.r2.shared.util.Error as BaseError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType @@ -48,7 +47,7 @@ public interface PublicationParser { public sealed class Error( public override val message: String, public override val cause: org.readium.r2.shared.util.Error? - ) : BaseError { + ) : org.readium.r2.shared.util.Error { public class UnsupportedFormat : Error("Asset format not supported.", null) diff --git a/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt b/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt index 835352c0fc..c28426cecb 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt @@ -16,7 +16,9 @@ import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar -import org.readium.r2.testapp.utils.extensions.readium.toDebugDescription +import org.readium.r2.testapp.domain.ImportUserError +import org.readium.r2.testapp.utils.extensions.readium.e +import org.readium.r2.testapp.utils.getUserMessage import timber.log.Timber class MainActivity : AppCompatActivity() { @@ -61,8 +63,8 @@ class MainActivity : AppCompatActivity() { getString(R.string.import_publication_success) is MainViewModel.Event.ImportPublicationError -> { - Timber.e(event.error.toDebugDescription(this)) - event.error.getUserMessage(this) + Timber.e(event.error) + ImportUserError(event.error).getUserMessage(this) } } Snackbar.make( diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index ff0bfd0da2..765986df07 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -9,12 +9,13 @@ package org.readium.r2.testapp import android.content.Context import android.view.View import org.readium.adapter.pdfium.document.PdfiumDocumentFactory -import org.readium.r2.lcp.LcpException +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpService import org.readium.r2.lcp.auth.LcpDialogAuthentication import org.readium.r2.navigator.preferences.FontFamily import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetriever +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.asset.CompositeResourceFactory @@ -73,7 +74,7 @@ class Readium(context: Context) { mediaTypeRetriever, downloadManager )?.let { Try.success(it) } - ?: Try.failure(LcpException.Unknown(Exception("liblcp is missing on the classpath"))) + ?: Try.failure(LcpError.Unknown(MessageError("liblcp is missing on the classpath"))) private val lcpDialogAuthentication = LcpDialogAuthentication() diff --git a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt index 08a913a9e3..d6e4d74ab8 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt @@ -28,8 +28,12 @@ import org.readium.r2.testapp.R import org.readium.r2.testapp.data.model.Book import org.readium.r2.testapp.databinding.FragmentBookshelfBinding import org.readium.r2.testapp.opds.GridAutoFitLayoutManager +import org.readium.r2.testapp.reader.OpeningUserError import org.readium.r2.testapp.reader.ReaderActivityContract +import org.readium.r2.testapp.utils.extensions.readium.e +import org.readium.r2.testapp.utils.getUserMessage import org.readium.r2.testapp.utils.viewLifecycle +import timber.log.Timber class BookshelfFragment : Fragment() { @@ -155,7 +159,8 @@ class BookshelfFragment : Fragment() { val message = when (event) { is BookshelfViewModel.Event.OpenPublicationError -> { - event.errorMessage + Timber.e(event.error) + OpeningUserError(event.error).getUserMessage(requireContext()) } is BookshelfViewModel.Event.LaunchReader -> { diff --git a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfViewModel.kt index a98821f0aa..e2801c0ecc 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfViewModel.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.launch import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.toUrl import org.readium.r2.testapp.data.model.Book +import org.readium.r2.testapp.reader.OpeningError import org.readium.r2.testapp.reader.ReaderActivityContract import org.readium.r2.testapp.utils.EventChannel @@ -49,9 +50,8 @@ class BookshelfViewModel(application: Application) : AndroidViewModel(applicatio viewModelScope.launch { val readerRepository = app.readerRepository.await() readerRepository.open(bookId) - .onFailure { error -> - val message = error.getUserMessage(app) - channel.send(Event.OpenPublicationError(message)) + .onFailure { + channel.send(Event.OpenPublicationError(it)) } .onSuccess { val arguments = ReaderActivityContract.Arguments(bookId) @@ -63,7 +63,7 @@ class BookshelfViewModel(application: Application) : AndroidViewModel(applicatio sealed class Event { class OpenPublicationError( - val errorMessage: String + val error: OpeningError ) : Event() class LaunchReader( diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index 03a379fd1f..772a94afce 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -15,10 +15,12 @@ import kotlinx.coroutines.launch import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetriever import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.data.FileSystemError +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.toUrl import org.readium.r2.streamer.PublicationFactory @@ -149,7 +151,7 @@ class Bookshelf( .getOrElse { return Try.failure( ImportError.PublicationError( - PublicationError.FsUnexpected(FileSystemError.IO(it)) + PublicationError.ReadError(ReadError.Access(FileSystemError.IO(it))) ) ) } @@ -164,7 +166,11 @@ class Bookshelf( ) if (id == -1L) { coverFile.delete() - return Try.failure(ImportError.DatabaseError()) + return Try.failure( + ImportError.DatabaseError( + MessageError("Could not insert book into database.") + ) + ) } } .onFailure { diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt index ad5363ea02..d8fc24cc00 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt @@ -6,24 +6,19 @@ package org.readium.r2.testapp.domain -import androidx.annotation.StringRes -import org.readium.r2.shared.UserException +import org.readium.r2.lcp.LcpError +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.downloads.DownloadManager -import org.readium.r2.testapp.R sealed class ImportError( - content: Content, - cause: Exception? -) : UserException(content, cause) { + override val cause: Error? +) : Error { - constructor(@StringRes userMessageId: Int) : - this(Content(userMessageId), null) - - constructor(cause: UserException) : - this(Content(cause), cause) + override val message: String = + "Import failed" class LcpAcquisitionFailed( - override val cause: UserException + override val cause: LcpError ) : ImportError(cause) class PublicationError( @@ -31,13 +26,12 @@ sealed class ImportError( ) : ImportError(cause) class DownloadFailed( - val error: DownloadManager.Error - ) : ImportError(R.string.import_publication_download_failed) + override val cause: DownloadManager.Error + ) : ImportError(cause) - class OpdsError( - override val cause: Throwable - ) : ImportError(R.string.import_publication_no_acquisition) + class OpdsError(override val cause: Error) : + ImportError(cause) - class DatabaseError : - ImportError(R.string.import_publication_unable_add_pub_database) + class DatabaseError(override val cause: Error) : + ImportError(cause) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportUserError.kt new file mode 100644 index 0000000000..0926e9183e --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportUserError.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.domain + +import androidx.annotation.StringRes +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError + +sealed class ImportUserError( + override val content: UserError.Content, + override val cause: UserError? +) : UserError { + + constructor(@StringRes userMessageId: Int) : + this(UserError.Content(userMessageId), null) + + constructor(cause: UserError) : + this(UserError.Content(cause), cause) + + class LcpAcquisitionFailed( + override val cause: LcpUserError + ) : ImportUserError(cause) + + class PublicationError( + override val cause: PublicationUserError + ) : ImportUserError(cause) + + class DownloadFailed( + val error: DownloadManager.Error + ) : ImportUserError(R.string.import_publication_download_failed) + + class OpdsError( + val error: Error + ) : ImportUserError(R.string.import_publication_no_acquisition) + + class DatabaseError : + ImportUserError(R.string.import_publication_unable_add_pub_database) + + companion object { + + operator fun invoke(error: ImportError): ImportUserError = + when (error) { + is ImportError.DatabaseError -> + DatabaseError() + is ImportError.DownloadFailed -> + DownloadFailed(error.cause) + is ImportError.LcpAcquisitionFailed -> + LcpAcquisitionFailed(LcpUserError(error.cause)) + is ImportError.OpdsError -> + OpdsError(error.cause) + is ImportError.PublicationError -> + PublicationError(PublicationUserError(error.cause)) + } + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/LcpUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/LcpUserError.kt new file mode 100644 index 0000000000..0a4de090a1 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/LcpUserError.kt @@ -0,0 +1,313 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.domain + +import androidx.annotation.PluralsRes +import androidx.annotation.StringRes +import java.util.Date +import org.readium.r2.lcp.LcpError +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Url +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError + +sealed class LcpUserError( + override val content: UserError.Content, + override val cause: UserError? = null +) : UserError { + + constructor(userMessageId: Int, vararg args: Any, quantity: Int? = null) : + this(UserError.Content(userMessageId, quantity, args)) + + constructor(@StringRes userMessageId: Int, vararg args: Any) : + this(UserError.Content(userMessageId, *args)) + + /** The interaction is not available with this License. */ + object LicenseInteractionNotAvailable : LcpUserError( + R.string.lcp_error_license_interaction_not_available + ) + + /** This License's profile is not supported by liblcp. */ + object LicenseProfileNotSupported : LcpUserError( + R.string.lcp_error_license_profile_not_supported + ) + + /** Failed to retrieve the Certificate Revocation List. */ + object CrlFetching : LcpUserError(R.string.lcp_error_crl_fetching) + + /** A network request failed with the given exception. */ + class Network(val error: Error?) : + LcpUserError(R.string.lcp_error_network) + + /** + * An unexpected LCP exception occurred. Please post an issue on r2-lcp-kotlin with the error + * message and how to reproduce it. + */ + class Runtime(val message: String) : + LcpUserError(R.string.lcp_error_runtime) + + /** An unknown low-level exception was reported. */ + class Unknown(val error: Error?) : + LcpUserError(R.string.lcp_error_unknown) + + /** + * Errors while checking the status of the License, using the Status Document. + * + * The app should notify the user and stop there. The message to the user must be clear about + * the status of the license: don't display "expired" if the status is "revoked". The date and + * time corresponding to the new status should be displayed (e.g. "The license expired on 01 + * January 2018"). + */ + sealed class LicenseStatus(userMessageId: Int, vararg args: Any, quantity: Int? = null) : + LcpUserError(userMessageId, args, quantity = quantity) { + + constructor(@StringRes userMessageId: Int, vararg args: Any) : + this(userMessageId, *args, quantity = null) + + constructor(@PluralsRes userMessageId: Int, quantity: Int, vararg args: Any) : + this(userMessageId, *args, quantity = quantity) + + class Cancelled(val date: Date) : + LicenseStatus(R.string.lcp_error_license_status_cancelled, date) + + class Returned(val date: Date) : + LicenseStatus(R.string.lcp_error_license_status_returned, date) + + class NotStarted(val start: Date) : + LicenseStatus(R.string.lcp_error_license_status_not_started, start) + + class Expired(val end: Date) : + LicenseStatus(R.string.lcp_error_license_status_expired, end) + + /** + * If the license has been revoked, the user message should display the number of devices which + * registered to the server. This count can be calculated from the number of "register" events + * in the status document. If no event is logged in the status document, no such message should + * appear (certainly not "The license was registered by 0 devices"). + */ + class Revoked(val date: Date, val devicesCount: Int) : + LicenseStatus( + R.plurals.lcp_error_license_status_revoked, + devicesCount, + date, + devicesCount + ) + } + + /** + * Errors while renewing a loan. + */ + sealed class Renew(@StringRes userMessageId: Int) : LcpUserError(userMessageId) { + + /** Your publication could not be renewed properly. */ + object RenewFailed : Renew(R.string.lcp_error_renew_renew_failed) + + /** Incorrect renewal period, your publication could not be renewed. */ + class InvalidRenewalPeriod(val maxRenewDate: Date?) : + Renew(R.string.lcp_error_renew_invalid_renewal_period) + + /** An unexpected error has occurred on the licensing server. */ + object UnexpectedServerError : + Renew(R.string.lcp_error_renew_unexpected_server_error) + } + + /** + * Errors while returning a loan. + */ + sealed class Return(@StringRes userMessageId: Int) : LcpUserError(userMessageId) { + + /** Your publication could not be returned properly. */ + object ReturnFailed : Return(R.string.lcp_error_return_return_failed) + + /** Your publication has already been returned before or is expired. */ + + object AlreadyReturnedOrExpired : + Return(R.string.lcp_error_return_already_returned_or_expired) + + /** An unexpected error has occurred on the licensing server. */ + object UnexpectedServerError : + Return(R.string.lcp_error_return_unexpected_server_error) + } + + /** + * Errors while parsing the License or Status JSON Documents. + */ + sealed class Parsing( + @StringRes userMessageId: Int = R.string.lcp_error_parsing + ) : LcpUserError(userMessageId) { + + /** The JSON is malformed and can't be parsed. */ + object MalformedJSON : Parsing(R.string.lcp_error_parsing_malformed_json) + + /** The JSON is not representing a valid License Document. */ + object LicenseDocument : Parsing(R.string.lcp_error_parsing_license_document) + + /** The JSON is not representing a valid Status Document. */ + object StatusDocument : Parsing(R.string.lcp_error_parsing_status_document) + + /** Invalid Link. */ + object Link : Parsing() + + /** Invalid Encryption. */ + object Encryption : Parsing() + + /** Invalid License Document Signature. */ + object Signature : Parsing() + + /** Invalid URL for link with [rel]. */ + class Url(val rel: String) : Parsing() + } + + /** + * Errors while reading or writing a LCP container (LCPL, EPUB, LCPDF, etc.) + */ + sealed class Container(@StringRes userMessageId: Int) : LcpUserError(userMessageId) { + + /** Can't access the container, it's format is wrong. */ + object OpenFailed : Container(R.string.lcp_error_container_open_failed) + + /** The file at given relative path is not found in the Container. */ + class FileNotFound(val url: Url) : Container(R.string.lcp_error_container_file_not_found) + + /** Can't read the file at given relative path in the Container. */ + class ReadFailed(val url: Url?) : Container(R.string.lcp_error_container_read_failed) + + /** Can't write the file at given relative path in the Container. */ + class WriteFailed(val url: Url?) : Container(R.string.lcp_error_container_write_failed) + } + + /** + * An error occurred while checking the integrity of the License, it can't be retrieved. + */ + sealed class LicenseIntegrity(@StringRes userMessageId: Int) : LcpUserError( + userMessageId + ) { + + object CertificateRevoked : + LicenseIntegrity(R.string.lcp_error_license_integrity_certificate_revoked) + + object InvalidCertificateSignature : + LicenseIntegrity(R.string.lcp_error_license_integrity_invalid_certificate_signature) + + object InvalidLicenseSignatureDate : + LicenseIntegrity(R.string.lcp_error_license_integrity_invalid_license_signature_date) + + object InvalidLicenseSignature : + LicenseIntegrity(R.string.lcp_error_license_integrity_invalid_license_signature) + + object InvalidUserKeyCheck : + LicenseIntegrity(R.string.lcp_error_license_integrity_invalid_user_key_check) + } + + sealed class Decryption(@StringRes userMessageId: Int) : LcpUserError(userMessageId) { + + object ContentKeyDecryptError : + Decryption(R.string.lcp_error_decryption_content_key_decrypt_error) + + object ContentDecryptError : + Decryption(R.string.lcp_error_decryption_content_decrypt_error) + } + + companion object { + + operator fun invoke(error: LcpError): LcpUserError = + when (error) { + is LcpError.Container -> + when (error) { + is LcpError.Container.FileNotFound -> + Container.FileNotFound(error.url) + LcpError.Container.OpenFailed -> + Container.OpenFailed + is LcpError.Container.ReadFailed -> + Container.ReadFailed(error.url) + is LcpError.Container.WriteFailed -> + Container.WriteFailed(error.url) + } + LcpError.CrlFetching -> + CrlFetching + is LcpError.Decryption -> + when (error) { + LcpError.Decryption.ContentDecryptError -> + Decryption.ContentDecryptError + LcpError.Decryption.ContentKeyDecryptError -> + Decryption.ContentKeyDecryptError + } + is LcpError.LicenseIntegrity -> + when (error) { + LcpError.LicenseIntegrity.CertificateRevoked -> + LicenseIntegrity.CertificateRevoked + LcpError.LicenseIntegrity.InvalidCertificateSignature -> + LicenseIntegrity.InvalidCertificateSignature + LcpError.LicenseIntegrity.InvalidLicenseSignature -> + LicenseIntegrity.InvalidLicenseSignature + LcpError.LicenseIntegrity.InvalidLicenseSignatureDate -> + LicenseIntegrity.InvalidLicenseSignatureDate + LcpError.LicenseIntegrity.InvalidUserKeyCheck -> + LicenseIntegrity.InvalidUserKeyCheck + } + LcpError.LicenseInteractionNotAvailable -> + LicenseInteractionNotAvailable + LcpError.LicenseProfileNotSupported -> + LicenseProfileNotSupported + is LcpError.LicenseStatus -> + when (error) { + is LcpError.LicenseStatus.Cancelled -> + LicenseStatus.Cancelled(error.date) + is LcpError.LicenseStatus.Expired -> + LicenseStatus.Expired(error.end) + is LcpError.LicenseStatus.NotStarted -> + LicenseStatus.NotStarted(error.start) + is LcpError.LicenseStatus.Returned -> + LicenseStatus.Returned(error.date) + is LcpError.LicenseStatus.Revoked -> + LicenseStatus.Revoked(error.date, error.devicesCount) + } + is LcpError.Network -> + Network(error.cause) + is LcpError.Parsing -> + when (error) { + LcpError.Parsing.Encryption -> + Parsing.Encryption + LcpError.Parsing.LicenseDocument -> + Parsing.LicenseDocument + LcpError.Parsing.Link -> + Parsing.Link + LcpError.Parsing.MalformedJSON -> + Parsing.MalformedJSON + LcpError.Parsing.Signature -> + Parsing.Signature + LcpError.Parsing.StatusDocument -> + Parsing.StatusDocument + is LcpError.Parsing.Url -> + Parsing.Url(error.rel) + } + + is LcpError.Renew -> + when (error) { + is LcpError.Renew.InvalidRenewalPeriod -> + Renew.InvalidRenewalPeriod(error.maxRenewDate) + LcpError.Renew.RenewFailed -> + Renew.RenewFailed + LcpError.Renew.UnexpectedServerError -> + Renew.UnexpectedServerError + } + is LcpError.Return -> + when (error) { + LcpError.Return.AlreadyReturnedOrExpired -> + Return.AlreadyReturnedOrExpired + LcpError.Return.ReturnFailed -> + Return.ReturnFailed + LcpError.Return.UnexpectedServerError -> + Return.UnexpectedServerError + } + is LcpError.Runtime -> + Runtime(error.message) + is LcpError.Unknown -> + Unknown(error.cause) + } + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/OpeningError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/OpeningError.kt deleted file mode 100644 index 7494f42615..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/OpeningError.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.testapp.domain - -import androidx.annotation.StringRes -import org.readium.r2.shared.UserException -import org.readium.r2.shared.util.Error -import org.readium.r2.testapp.R - -sealed class OpeningError( - content: Content, - cause: Exception? -) : UserException(content, cause) { - - constructor(@StringRes userMessageId: Int) : - this(Content(userMessageId), null) - - constructor(cause: UserException) : - this(Content(cause), cause) - - class PublicationError( - override val cause: org.readium.r2.testapp.domain.PublicationError - ) : OpeningError(cause) - - class AudioEngineInitialization( - val error: Error - ) : OpeningError(R.string.opening_publication_audio_engine_initialization) -} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index e4c37c6cc0..0a0e320d94 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -6,68 +6,42 @@ package org.readium.r2.testapp.domain -import androidx.annotation.StringRes -import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetriever import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.data.ContentProviderError -import org.readium.r2.shared.util.data.FileSystemError -import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.http.HttpError -import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.streamer.PublicationFactory -import org.readium.r2.testapp.R -sealed class PublicationError(@StringRes userMessageId: Int) : UserException(userMessageId) { +sealed class PublicationError( + override val message: String, + override val cause: Error? = null +) : Error { - class HttpNotFound(val error: Error) : - PublicationError(R.string.publication_error_network_not_found) + class ReadError(override val cause: org.readium.r2.shared.util.data.ReadError) : + PublicationError(cause.message, cause.cause) - class HttpForbidden(val error: Error) : - PublicationError(R.string.publication_error_network_forbidden) + class UnsupportedScheme(cause: Error) : + PublicationError(cause.message, cause.cause) - class HttpConnectivity(val error: Error) : - PublicationError(R.string.publication_error_network_connectivity) + class UnsupportedContentProtection(cause: Error) : + PublicationError(cause.message, cause.cause) + class UnsupportedArchiveFormat(cause: Error) : + PublicationError(cause.message, cause.cause) - class HttpUnexpected(val error: Error) : - PublicationError(R.string.publication_error_network_unexpected) + class UnsupportedPublication(cause: Error) : + PublicationError(cause.message, cause.cause) - class FsNotFound(val error: Error) : - PublicationError(R.string.publication_error_filesystem_not_found) + class InvalidPublication(cause: Error) : + PublicationError(cause.message, cause.cause) - class FsUnexpected(val error: Error) : - PublicationError(R.string.publication_error_filesystem_unexpected) - - class OutOfMemory(val error: Error) : - PublicationError(R.string.publication_error_out_of_memory) - - class UnsupportedScheme(val error: Error) : - PublicationError(R.string.publication_error_scheme_not_supported) - - class UnsupportedContentProtection(val error: Error? = null) : - PublicationError(R.string.publication_error_unsupported_protection) - class UnsupportedArchiveFormat(val error: Error) : - PublicationError(R.string.publication_error_unsupported_archive) - - class UnsupportedPublication(val error: Error? = null) : - PublicationError(R.string.publication_error_unsupported_asset) - - class InvalidPublication(val error: Error) : - PublicationError(R.string.publication_error_invalid_publication) - - class RestrictedPublication(val error: Error? = null) : - PublicationError(R.string.publication_error_restricted) - - class Unexpected(val error: Error) : - PublicationError(R.string.publication_error_unexpected) + class Unexpected(cause: Error) : + PublicationError(cause.message, cause.cause) companion object { operator fun invoke(error: AssetRetriever.Error): PublicationError = when (error) { is AssetRetriever.Error.AccessError -> - PublicationError(error.cause) + PublicationError(error) is AssetRetriever.Error.ArchiveFormatNotSupported -> UnsupportedArchiveFormat(error) is AssetRetriever.Error.SchemeNotSupported -> @@ -77,68 +51,19 @@ sealed class PublicationError(@StringRes userMessageId: Int) : UserException(use operator fun invoke(error: ContentProtectionSchemeRetriever.Error): PublicationError = when (error) { is ContentProtectionSchemeRetriever.Error.AccessError -> - PublicationError(error.cause) + PublicationError(error) ContentProtectionSchemeRetriever.Error.NoContentProtectionFound -> - UnsupportedContentProtection() + UnsupportedContentProtection(error) } operator fun invoke(error: PublicationFactory.Error): PublicationError = when (error) { is PublicationFactory.Error.ReadError -> - PublicationError(error.cause) + PublicationError(error) is PublicationFactory.Error.UnsupportedAsset -> UnsupportedPublication(error) is PublicationFactory.Error.UnsupportedContentProtection -> UnsupportedContentProtection(error) } - - operator fun invoke(error: ReadError): PublicationError = - when (error) { - is ReadError.Access -> - when (val cause = error.cause) { - is HttpError -> PublicationError(cause) - is FileSystemError -> PublicationError(cause) - is ContentProviderError -> PublicationError(cause) - else -> Unexpected(cause) - } - is ReadError.Decoding -> InvalidPublication(error) - is ReadError.Other -> Unexpected(error) - is ReadError.OutOfMemory -> OutOfMemory(error) - is ReadError.UnsupportedOperation -> Unexpected(error) - } - - private operator fun invoke(error: HttpError): PublicationError = - when (error) { - is HttpError.IO -> - HttpUnexpected(error) - is HttpError.MalformedResponse -> - HttpUnexpected(error) - is HttpError.Redirection -> - HttpUnexpected(error) - is HttpError.Timeout -> - HttpConnectivity(error) - is HttpError.UnreachableHost -> - HttpConnectivity(error) - is HttpError.Response -> - when (error.status) { - HttpStatus.Forbidden -> HttpForbidden(error) - HttpStatus.NotFound -> HttpNotFound(error) - else -> HttpUnexpected(error) - } - } - - private operator fun invoke(error: FileSystemError): PublicationError = - when (error) { - is FileSystemError.Forbidden -> FsUnexpected(error) - is FileSystemError.IO -> FsUnexpected(error) - is FileSystemError.NotFound -> FsNotFound(error) - } - - private operator fun invoke(error: ContentProviderError): PublicationError = - when (error) { - is ContentProviderError.FileNotFound -> FsNotFound(error) - is ContentProviderError.IO -> FsUnexpected(error) - is ContentProviderError.NotAvailable -> FsUnexpected(error) - } } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index 0f7cdddc03..2fbd3ff3d6 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -13,18 +13,20 @@ import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch -import org.readium.r2.lcp.LcpException +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpPublicationRetriever as ReadiumLcpPublicationRetriever import org.readium.r2.lcp.LcpService import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.opds.images import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.data.FileSystemError +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry @@ -125,7 +127,7 @@ class LocalPublicationRetriever( .getOrElse { listener.onError( ImportError.PublicationError( - PublicationError.FsUnexpected(FileSystemError.IO(it)) + PublicationError.ReadError(ReadError.Access(FileSystemError.IO(it))) ) ) return@launch @@ -165,7 +167,11 @@ class LocalPublicationRetriever( ) { if (lcpPublicationRetriever == null) { listener.onError( - ImportError.PublicationError(PublicationError.UnsupportedContentProtection()) + ImportError.PublicationError( + PublicationError.UnsupportedContentProtection( + MessageError("LCP support is missing.") + ) + ) ) } else { lcpPublicationRetriever.retrieve(sourceAsset, tempFile, coverUrl) @@ -184,7 +190,9 @@ class LocalPublicationRetriever( tryOrNull { libraryFile.delete() } listener.onError( ImportError.PublicationError( - PublicationError.FsUnexpected(ThrowableError(e)) + PublicationError.ReadError( + ReadError.Access(FileSystemError.IO(e)) + ) ) ) return @@ -261,11 +269,11 @@ class OpdsPublicationRetriever( } } - private fun Publication.acquisitionUrl(): Try { + private fun Publication.acquisitionUrl(): Try { val acquisitionUrl = links .filter { it.mediaType?.isPublication == true || it.mediaType == MediaType.LCP_LICENSE_DOCUMENT } .firstNotNullOfOrNull { it.url() as? AbsoluteUrl } - ?: return Try.failure(Exception("No supported link to acquire publication.")) + ?: return Try.failure(MessageError("No supported link to acquire publication.")) return Try.success(acquisitionUrl) } @@ -349,16 +357,17 @@ class LcpPublicationRetriever( coroutineScope.launch { val license = licenceAsset.resource.read() .getOrElse { - listener.onError(ImportError.PublicationError(PublicationError(it))) + listener.onError(ImportError.PublicationError(PublicationError.ReadError(it))) return@launch } .let { - try { - LicenseDocument(it) - } catch (e: LcpException) { - listener.onError(ImportError.LcpAcquisitionFailed(e)) - return@launch - } + LicenseDocument.fromBytes(it) + .getOrElse { error -> + listener.onError( + ImportError.LcpAcquisitionFailed(error) + ) + return@launch + } } tryOrNull { licenceFile.delete() } @@ -400,11 +409,13 @@ class LcpPublicationRetriever( override fun onAcquisitionFailed( requestId: ReadiumLcpPublicationRetriever.RequestId, - error: LcpException + error: LcpError ) { coroutineScope.launch { downloadRepository.remove(requestId.value) - listener.onError(ImportError.LcpAcquisitionFailed(error)) + listener.onError( + ImportError.LcpAcquisitionFailed(error) + ) } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationUserError.kt new file mode 100644 index 0000000000..1912bda044 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationUserError.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.domain + +import androidx.annotation.StringRes +import org.readium.r2.shared.util.Error +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError + +sealed class PublicationUserError( + override val content: UserError.Content, + override val cause: UserError? = null +) : UserError { + + constructor(@StringRes userMessageId: Int) : + this(UserError.Content(userMessageId), null) + + class ReadError(cause: UserError) : + PublicationUserError(cause.content, cause.cause) + + class UnsupportedScheme(val error: Error) : + PublicationUserError(R.string.publication_error_scheme_not_supported) + + class UnsupportedContentProtection(val error: Error? = null) : + PublicationUserError(R.string.publication_error_unsupported_protection) + class UnsupportedArchiveFormat(val error: Error) : + PublicationUserError(R.string.publication_error_unsupported_archive) + + class UnsupportedPublication(val error: Error? = null) : + PublicationUserError(R.string.publication_error_unsupported_asset) + + class InvalidPublication(val error: Error) : + PublicationUserError(R.string.publication_error_invalid_publication) + + class Unexpected(val error: Error) : + PublicationUserError(R.string.publication_error_unexpected) + + companion object { + + operator fun invoke(error: PublicationError): PublicationUserError = + when (error) { + is PublicationError.InvalidPublication -> + InvalidPublication(error) + + is PublicationError.Unexpected -> + Unexpected(error) + + is PublicationError.UnsupportedArchiveFormat -> + UnsupportedArchiveFormat(error) + + is PublicationError.UnsupportedContentProtection -> + UnsupportedContentProtection(error) + + is PublicationError.UnsupportedPublication -> + UnsupportedPublication(error) + + is PublicationError.UnsupportedScheme -> + UnsupportedScheme(error) + + is PublicationError.ReadError -> + ReadError(ReadUserError(error.cause)) + } + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt new file mode 100644 index 0000000000..995c410ba2 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.domain + +import androidx.annotation.StringRes +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.data.ContentProviderError +import org.readium.r2.shared.util.data.FileSystemError +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.http.HttpStatus +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError + +sealed class ReadUserError( + override val content: UserError.Content, + override val cause: UserError? = null +) : UserError { + constructor(@StringRes userMessageId: Int) : + this(UserError.Content(userMessageId), null) + + class HttpNotFound(val error: Error) : + ReadUserError(R.string.publication_error_network_not_found) + + class HttpForbidden(val error: Error) : + ReadUserError(R.string.publication_error_network_forbidden) + + class HttpConnectivity(val error: Error) : + ReadUserError(R.string.publication_error_network_connectivity) + + class HttpUnexpected(val error: Error) : + ReadUserError(R.string.publication_error_network_unexpected) + + class FsNotFound(val error: Error) : + ReadUserError(R.string.publication_error_filesystem_not_found) + + class FsUnexpected(val error: Error) : + ReadUserError(R.string.publication_error_filesystem_unexpected) + + class OutOfMemory(val error: Error) : + ReadUserError(R.string.publication_error_out_of_memory) + + class InvalidPublication(val error: Error) : + ReadUserError(R.string.publication_error_invalid_publication) + + class Unexpected(val error: Error) : + ReadUserError(R.string.publication_error_unexpected) + + companion object { + + operator fun invoke(error: ReadError): ReadUserError = + when (error) { + is ReadError.Access -> + when (val cause = error.cause) { + is HttpError -> ReadUserError(cause) + is FileSystemError -> ReadUserError(cause) + is ContentProviderError -> ReadUserError(cause) + else -> Unexpected(cause) + } + is ReadError.Decoding -> InvalidPublication(error) + is ReadError.Other -> Unexpected(error) + is ReadError.OutOfMemory -> OutOfMemory(error) + is ReadError.UnsupportedOperation -> Unexpected(error) + } + + private operator fun invoke(error: HttpError): ReadUserError = + when (error) { + is HttpError.IO -> + HttpUnexpected(error) + is HttpError.MalformedResponse -> + HttpUnexpected(error) + is HttpError.Redirection -> + HttpUnexpected(error) + is HttpError.Timeout -> + HttpConnectivity(error) + is HttpError.UnreachableHost -> + HttpConnectivity(error) + is HttpError.Response -> + when (error.status) { + HttpStatus.Forbidden -> HttpForbidden(error) + HttpStatus.NotFound -> HttpNotFound(error) + else -> HttpUnexpected(error) + } + } + + private operator fun invoke(error: FileSystemError): ReadUserError = + when (error) { + is FileSystemError.Forbidden -> FsUnexpected(error) + is FileSystemError.IO -> FsUnexpected(error) + is FileSystemError.NotFound -> FsNotFound(error) + } + + private operator fun invoke(error: ContentProviderError): ReadUserError = + when (error) { + is ContentProviderError.FileNotFound -> FsNotFound(error) + is ContentProviderError.IO -> FsUnexpected(error) + is ContentProviderError.NotAvailable -> FsUnexpected(error) + } + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/SearchError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/SearchError.kt deleted file mode 100644 index 665f8d08d4..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/SearchError.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.readium.r2.testapp.domain - -import androidx.annotation.StringRes -import org.readium.r2.shared.UserException -import org.readium.r2.shared.util.Error -import org.readium.r2.testapp.R - -sealed class SearchError(@StringRes userMessageId: Int) : UserException(userMessageId) { - - object PublicationNotSearchable : - SearchError(R.string.search_error_not_searchable) - - class BadQuery(val error: Error) : - SearchError(R.string.search_error_not_searchable) - - class ResourceError(val error: Error) : - SearchError(R.string.search_error_other) - - class NetworkError(val error: Error) : - SearchError(R.string.search_error_other) - - object Cancelled : - SearchError(R.string.search_error_cancelled) - - class Other(val error: Error) : - SearchError(R.string.search_error_other) -} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/TtsError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/TtsError.kt deleted file mode 100644 index 7db8b97144..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/TtsError.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.readium.r2.testapp.domain - -import androidx.annotation.StringRes -import org.readium.navigator.media.tts.TtsNavigator -import org.readium.navigator.media.tts.android.AndroidTtsEngine -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.UserException -import org.readium.r2.testapp.R - -@OptIn(ExperimentalReadiumApi::class) -sealed class TtsError(@StringRes userMessageId: Int) : UserException(userMessageId) { - - class ContentError(val error: TtsNavigator.State.Error.ContentError) : - TtsError(R.string.tts_error_other) - - sealed class EngineError(@StringRes userMessageId: Int) : TtsError(userMessageId) { - - class Network(val error: AndroidTtsEngine.Error.Network) : - EngineError(R.string.tts_error_network) - - class Other(val error: AndroidTtsEngine.Error) : - EngineError(R.string.tts_error_other) - } - - class ServiceError(val exception: Exception) : - TtsError(R.string.error_unexpected) - - class Initialization() : - TtsError(R.string.tts_error_initialization) -} diff --git a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt index e6351acaba..685abcb617 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt @@ -22,10 +22,13 @@ import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat import org.readium.r2.lcp.MaterialRenewListener import org.readium.r2.lcp.lcpLicense -import org.readium.r2.shared.UserException +import org.readium.r2.shared.util.Error import org.readium.r2.testapp.R import org.readium.r2.testapp.databinding.FragmentDrmManagementBinding import org.readium.r2.testapp.reader.ReaderViewModel +import org.readium.r2.testapp.utils.UserError +import org.readium.r2.testapp.utils.extensions.readium.w +import org.readium.r2.testapp.utils.getUserMessage import org.readium.r2.testapp.utils.viewLifecycle import timber.log.Timber @@ -104,8 +107,8 @@ class DrmManagementFragment : Fragment() { model.renewLoan(this@DrmManagementFragment) .onSuccess { newDate -> binding.drmValueEnd.text = newDate.toFormattedString() - }.onFailure { exception -> - exception.toastUserMessage(requireView()) + }.onFailure { error -> + error.toastUserMessage(requireView()) } } } @@ -135,10 +138,10 @@ private fun Date?.toFormattedString() = DateTime(this).toString(DateTimeFormat.shortDateTime()).orEmpty() // FIXME: the toast is drawn behind the navigation bar -private fun Exception.toastUserMessage(view: View) { - if (this is UserException) { +private fun Error.toastUserMessage(view: View) { + if (this is UserError) { Snackbar.make(view, getUserMessage(view.context), Snackbar.LENGTH_LONG).show() } - Timber.d(this) + Timber.w(this) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt index bc14687812..762b5c9839 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt @@ -9,6 +9,8 @@ package org.readium.r2.testapp.drm import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel import java.util.* +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try abstract class DrmManagementViewModel : ViewModel() { @@ -33,11 +35,11 @@ abstract class DrmManagementViewModel : ViewModel() { open val canRenewLoan: Boolean = false - open suspend fun renewLoan(fragment: Fragment): Try = - Try.failure(Exception("Renewing a loan is not supported")) + open suspend fun renewLoan(fragment: Fragment): Try = + Try.failure(MessageError("Renewing a loan is not supported")) open val canReturnPublication: Boolean = false - open suspend fun returnPublication(): Try = - Try.failure(Exception("Returning a publication is not supported")) + open suspend fun returnPublication(): Try = + Try.failure(MessageError("Returning a publication is not supported")) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/drm/LcpManagementViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/drm/LcpManagementViewModel.kt index 86b9a31ca4..b4f4790591 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/drm/LcpManagementViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/drm/LcpManagementViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import java.util.* import org.readium.r2.lcp.LcpLicense +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try class LcpManagementViewModel( @@ -64,13 +65,13 @@ class LcpManagementViewModel( override val canRenewLoan: Boolean get() = lcpLicense.canRenewLoan - override suspend fun renewLoan(fragment: Fragment): Try { + override suspend fun renewLoan(fragment: Fragment): Try { return lcpLicense.renewLoan(renewListener) } override val canReturnPublication: Boolean get() = lcpLicense.canReturnPublication - override suspend fun returnPublication(): Try = + override suspend fun returnPublication(): Try = lcpLicense.returnPublication() } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/BaseReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/BaseReaderFragment.kt index 22ef2f1910..355fd4205b 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/BaseReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/BaseReaderFragment.kt @@ -20,13 +20,12 @@ import org.readium.r2.lcp.lcpLicense import org.readium.r2.navigator.Navigator import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.testapp.R import org.readium.r2.testapp.reader.preferences.UserPreferencesBottomSheetDialogFragment -import org.readium.r2.testapp.utils.extensions.readium.toDebugDescription -import timber.log.Timber +import org.readium.r2.testapp.utils.UserError +import org.readium.r2.testapp.utils.getUserMessage /* * Base reader fragment class @@ -117,9 +116,8 @@ abstract class BaseReaderFragment : Fragment() { navigator.go(locator, animated) } - protected fun showError(error: UserException) { + protected fun showError(error: UserError) { val context = context ?: return - Timber.e(error.toDebugDescription(context)) Toast.makeText(context, error.getUserMessage(context), Toast.LENGTH_LONG).show() } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt new file mode 100644 index 0000000000..182d9c2871 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.reader + +import org.readium.r2.shared.util.Error + +sealed class OpeningError( + override val cause: Error? +) : Error { + + override val message: String = + "Could not open publication" + + class PublicationError( + override val cause: org.readium.r2.testapp.domain.PublicationError + ) : OpeningError(cause) + + class RestrictedPublication( + cause: Error + ) : OpeningError(cause) + + class AudioEngineInitialization( + cause: Error + ) : OpeningError(cause) +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningUserError.kt new file mode 100644 index 0000000000..ec6cd3309c --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningUserError.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.reader + +import androidx.annotation.StringRes +import org.readium.r2.shared.util.Error +import org.readium.r2.testapp.R +import org.readium.r2.testapp.domain.PublicationUserError +import org.readium.r2.testapp.utils.UserError + +sealed class OpeningUserError( + override val content: UserError.Content, + override val cause: UserError? +) : UserError { + + constructor(@StringRes userMessageId: Int) : + this(UserError.Content(userMessageId), null) + + constructor(cause: UserError) : + this(UserError.Content(cause), cause) + + class PublicationError( + override val cause: PublicationUserError + ) : OpeningUserError(cause) + + class RestrictedPublication(val error: Error? = null) : + OpeningUserError(R.string.publication_error_restricted) + + class AudioEngineInitialization( + val error: Error + ) : OpeningUserError(R.string.opening_publication_audio_engine_initialization) + + companion object { + + operator fun invoke(error: OpeningError): OpeningUserError = + when (error) { + is OpeningError.AudioEngineInitialization -> + AudioEngineInitialization(error) + is OpeningError.PublicationError -> + PublicationError(PublicationUserError(error.cause)) + is OpeningError.RestrictedPublication -> + RestrictedPublication(error) + } + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivity.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivity.kt index 820eaabcb2..0045fee82a 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivity.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivity.kt @@ -18,8 +18,6 @@ import androidx.fragment.app.FragmentResultListener import androidx.fragment.app.commit import androidx.fragment.app.commitNow import androidx.lifecycle.ViewModelProvider -import org.readium.navigator.media2.ExperimentalMedia2 -import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.util.toUri import org.readium.r2.testapp.Application @@ -29,9 +27,9 @@ import org.readium.r2.testapp.drm.DrmManagementContract import org.readium.r2.testapp.drm.DrmManagementFragment import org.readium.r2.testapp.outline.OutlineContract import org.readium.r2.testapp.outline.OutlineFragment -import org.readium.r2.testapp.utils.extensions.readium.toDebugDescription +import org.readium.r2.testapp.utils.UserError +import org.readium.r2.testapp.utils.getUserMessage import org.readium.r2.testapp.utils.launchWebBrowser -import timber.log.Timber /* * An activity to read a publication @@ -112,7 +110,6 @@ open class ReaderActivity : AppCompatActivity() { } } - @OptIn(ExperimentalMedia2::class) private fun createReaderFragment(readerData: ReaderInitData): BaseReaderFragment? { val readerClass: Class? = when (readerData) { is EpubReaderInitData -> EpubReaderFragment::class.java @@ -171,8 +168,7 @@ open class ReaderActivity : AppCompatActivity() { } } - private fun showError(error: UserException) { - Timber.e(error.toDebugDescription(this)) + private fun showError(error: UserError) { Toast.makeText(this, error.getUserMessage(this), Toast.LENGTH_LONG).show() } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt index 5cdacfca39..ab98cc0454 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt @@ -21,11 +21,12 @@ import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.allAreHtml import org.readium.r2.shared.publication.services.isRestricted +import org.readium.r2.shared.publication.services.protectionError +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrElse import org.readium.r2.testapp.Readium import org.readium.r2.testapp.data.BookRepository -import org.readium.r2.testapp.domain.OpeningError import org.readium.r2.testapp.domain.PublicationError import org.readium.r2.testapp.reader.preferences.AndroidTtsPreferencesManagerFactory import org.readium.r2.testapp.reader.preferences.EpubPreferencesManagerFactory @@ -98,8 +99,9 @@ class ReaderRepository( // The publication is protected with a DRM and not unlocked. if (publication.isRestricted) { return Try.failure( - OpeningError.PublicationError( - PublicationError.RestrictedPublication() + OpeningError.RestrictedPublication( + publication.protectionError + ?: MessageError("Publication is restricted.") ) ) } @@ -119,7 +121,9 @@ class ReaderRepository( else -> Try.failure( OpeningError.PublicationError( - PublicationError.UnsupportedPublication() + PublicationError.UnsupportedPublication( + MessageError("No navigator supports this publication.") + ) ) ) } @@ -140,7 +144,11 @@ class ReaderRepository( publication, ExoPlayerEngineProvider(application) ) ?: return Try.failure( - OpeningError.PublicationError(PublicationError.UnsupportedPublication()) + OpeningError.PublicationError( + PublicationError.UnsupportedPublication( + MessageError("Cannot create audio navigator factory.") + ) + ) ) val navigator = navigatorFactory.createNavigator( @@ -152,7 +160,7 @@ class ReaderRepository( is AudioNavigatorFactory.Error.EngineInitialization -> OpeningError.AudioEngineInitialization(it) is AudioNavigatorFactory.Error.UnsupportedPublication -> - OpeningError.PublicationError(PublicationError.UnsupportedPublication()) + OpeningError.PublicationError(PublicationError.UnsupportedPublication(it)) } ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt index 7ce6aedb13..9d8550ee55 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt @@ -23,11 +23,9 @@ import org.readium.r2.navigator.epub.EpubNavigatorFragment import org.readium.r2.navigator.image.ImageNavigatorFragment import org.readium.r2.navigator.pdf.PdfNavigatorFragment import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.UserException import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.LocatorCollection import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.services.search.SearchError import org.readium.r2.shared.publication.services.search.SearchIterator import org.readium.r2.shared.publication.services.search.SearchTry import org.readium.r2.shared.publication.services.search.search @@ -36,14 +34,18 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ReadError import org.readium.r2.testapp.Application +import org.readium.r2.testapp.R import org.readium.r2.testapp.data.BookRepository import org.readium.r2.testapp.data.model.Highlight -import org.readium.r2.testapp.domain.PublicationError +import org.readium.r2.testapp.domain.ReadUserError import org.readium.r2.testapp.reader.preferences.UserPreferencesViewModel import org.readium.r2.testapp.reader.tts.TtsViewModel import org.readium.r2.testapp.search.SearchPagingSource +import org.readium.r2.testapp.search.SearchUserError import org.readium.r2.testapp.utils.EventChannel +import org.readium.r2.testapp.utils.UserError import org.readium.r2.testapp.utils.createViewModelFactory +import org.readium.r2.testapp.utils.extensions.readium.e import timber.log.Timber @OptIn( @@ -60,6 +62,14 @@ class ReaderViewModel( ImageNavigatorFragment.Listener, PdfNavigatorFragment.Listener { + class ReaderUserError( + override val cause: UserError + ) : UserError { + + override val content: UserError.Content = + UserError.Content(R.string.reader_error) + } + val readerInitData = try { checkNotNull(readerRepository[bookId]) @@ -211,28 +221,15 @@ class ReaderViewModel( lastSearchQuery = query _searchLocators.value = emptyList() searchIterator = publication.search(query) - .onFailure { activityChannel.send(ActivityCommand.ToastError(it.wrap())) } + .onFailure { + Timber.e(it) + activityChannel.send(ActivityCommand.ToastError(SearchUserError(it))) + } .getOrNull() pagingSourceFactory.invalidate() searchChannel.send(SearchCommand.StartNewSearch) } - private fun SearchError.wrap(): org.readium.r2.testapp.domain.SearchError = - when (this) { - is SearchError.BadQuery -> - org.readium.r2.testapp.domain.SearchError.BadQuery(this) - SearchError.Cancelled -> - org.readium.r2.testapp.domain.SearchError.Cancelled - is SearchError.NetworkError -> - org.readium.r2.testapp.domain.SearchError.NetworkError(this) - is SearchError.Other -> - org.readium.r2.testapp.domain.SearchError.Other(this) - SearchError.PublicationNotSearchable -> - org.readium.r2.testapp.domain.SearchError.PublicationNotSearchable - is SearchError.ResourceError -> - org.readium.r2.testapp.domain.SearchError.ResourceError(this) - } - fun cancelSearch() = viewModelScope.launch { _searchLocators.value = emptyList() searchIterator?.close() @@ -272,9 +269,10 @@ class ReaderViewModel( // Navigator.Listener override fun onResourceLoadFailed(href: Url, error: ReadError) { + Timber.e(error) activityChannel.send( ActivityCommand.ToastError( - PublicationError(error) + ReaderUserError(ReadUserError(error)) ) ) } @@ -305,7 +303,7 @@ class ReaderViewModel( object OpenOutlineRequested : ActivityCommand() object OpenDrmManagementRequested : ActivityCommand() class OpenExternalLink(val url: AbsoluteUrl) : ActivityCommand() - class ToastError(val error: UserException) : ActivityCommand() + class ToastError(val error: UserError) : ActivityCommand() } sealed class FeedbackEvent { diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt index 2de829ed22..e9a77e1a15 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt @@ -55,10 +55,13 @@ import org.readium.r2.testapp.data.model.Highlight import org.readium.r2.testapp.databinding.FragmentReaderBinding import org.readium.r2.testapp.reader.preferences.UserPreferencesBottomSheetDialogFragment import org.readium.r2.testapp.reader.tts.TtsControls +import org.readium.r2.testapp.reader.tts.TtsUserError import org.readium.r2.testapp.reader.tts.TtsViewModel import org.readium.r2.testapp.utils.* import org.readium.r2.testapp.utils.extensions.confirmDialog +import org.readium.r2.testapp.utils.extensions.readium.e import org.readium.r2.testapp.utils.extensions.throttleLatest +import timber.log.Timber /* * Base reader fragment class @@ -215,9 +218,10 @@ abstract class VisualReaderFragment : BaseReaderFragment() { events .onEach { event -> when (event) { - is TtsViewModel.Event.OnError -> - showError(event.error) - + is TtsViewModel.Event.OnError -> { + Timber.e(event.error) + showError(TtsUserError(event.error)) + } is TtsViewModel.Event.OnMissingVoiceData -> confirmAndInstallTtsVoice(event.language) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsError.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsError.kt new file mode 100644 index 0000000000..9e6e09857f --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsError.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.reader.tts + +import org.readium.navigator.media.tts.TtsNavigator +import org.readium.navigator.media.tts.TtsNavigatorFactory +import org.readium.navigator.media.tts.android.AndroidTtsEngine +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.ThrowableError + +@OptIn(ExperimentalReadiumApi::class) +sealed class TtsError( + override val message: String, + override val cause: Error? = null +) : Error { + + class ContentError(override val cause: TtsNavigator.Error.ContentError) : + TtsError(cause.message, cause.cause) + + sealed class EngineError(override val cause: AndroidTtsEngine.Error) : + TtsError(cause.message, cause.cause) { + + class Network(override val cause: AndroidTtsEngine.Error.Network) : + EngineError(cause) + + class Other(override val cause: AndroidTtsEngine.Error) : + EngineError(cause) + } + + class ServiceError(val exception: Exception) : + TtsError("Could not open session.", ThrowableError(exception)) + + class Initialization(override val cause: TtsNavigatorFactory.Error) : + TtsError(cause.message, cause) +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsUserError.kt new file mode 100644 index 0000000000..f64caad0f4 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsUserError.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.reader.tts + +import androidx.annotation.StringRes +import org.readium.navigator.media.tts.TtsNavigator +import org.readium.navigator.media.tts.android.AndroidTtsEngine +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.util.Error +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError + +@OptIn(ExperimentalReadiumApi::class) +sealed class TtsUserError( + override val content: UserError.Content, + override val cause: UserError? = null +) : UserError { + + constructor(@StringRes userMessageId: Int) : + this(UserError.Content(userMessageId), null) + + class ContentError(val error: TtsNavigator.Error.ContentError) : + TtsUserError(R.string.tts_error_other) + + sealed class EngineError(@StringRes userMessageId: Int) : TtsUserError(userMessageId) { + + class Network(val error: AndroidTtsEngine.Error.Network) : + EngineError(R.string.tts_error_network) + + class Other(val error: AndroidTtsEngine.Error) : + EngineError(R.string.tts_error_other) + } + + class ServiceError(val error: Error?) : + TtsUserError(R.string.error_unexpected) + + class Initialization(val error: Error) : + TtsUserError(R.string.tts_error_initialization) + + companion object { + + operator fun invoke(error: TtsError): TtsUserError = + when (error) { + is TtsError.ContentError -> + ContentError(error.cause) + is TtsError.EngineError.Network -> + EngineError.Network(error.cause) + is TtsError.EngineError.Other -> + EngineError.Other(error.cause) + is TtsError.Initialization -> + Initialization(error.cause) + is TtsError.ServiceError -> + ServiceError(error.cause) + } + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt index 350bb4ce17..658f3ac102 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsViewModel.kt @@ -26,7 +26,6 @@ import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Language import org.readium.r2.shared.util.getOrElse -import org.readium.r2.testapp.domain.TtsError import org.readium.r2.testapp.reader.MediaService import org.readium.r2.testapp.reader.MediaServiceFacade import org.readium.r2.testapp.reader.ReaderInitData @@ -147,7 +146,7 @@ class TtsViewModel private constructor( } is MediaNavigator.State.Error -> { onPlaybackError( - playback.state as TtsNavigator.State.Error + (playback.state as TtsNavigator.State.Error).error ) } is MediaNavigator.State.Ready -> {} @@ -182,7 +181,7 @@ class TtsViewModel private constructor( initialLocator = start, initialPreferences = preferencesManager.preferences.value ).getOrElse { - val error = TtsError.Initialization() + val error = TtsError.Initialization(it) _events.send(Event.OnError(error)) return } @@ -191,8 +190,8 @@ class TtsViewModel private constructor( mediaServiceFacade.openSession(bookId, ttsNavigator) } catch (e: Exception) { ttsNavigator.close() - val exception = TtsError.ServiceError(e) - _events.trySend(Event.OnError(exception)) + val error = TtsError.ServiceError(e) + _events.trySend(Event.OnError(error)) launchJob = null return } @@ -225,13 +224,13 @@ class TtsViewModel private constructor( stop() } - private fun onPlaybackError(error: TtsNavigator.State.Error) { + private fun onPlaybackError(error: TtsNavigator.Error) { val event = when (error) { - is TtsNavigator.State.Error.ContentError -> { + is TtsNavigator.Error.ContentError -> { Event.OnError(TtsError.ContentError(error)) } - is TtsNavigator.State.Error.EngineError<*> -> { - val engineError = (error.error as AndroidTtsEngine.Error) + is TtsNavigator.Error.EngineError<*> -> { + val engineError = (error.cause as AndroidTtsEngine.Error) when (engineError) { is AndroidTtsEngine.Error.LanguageMissingData -> Event.OnMissingVoiceData(engineError.language) diff --git a/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt new file mode 100644 index 0000000000..bf982b7577 --- /dev/null +++ b/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2020 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.testapp.search + +import androidx.annotation.StringRes +import org.readium.r2.shared.ExperimentalReadiumApi +import org.readium.r2.shared.publication.services.search.SearchError +import org.readium.r2.shared.util.Error +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError + +sealed class SearchUserError( + override val content: UserError.Content, + override val cause: UserError? = null +) : UserError { + constructor(@StringRes userMessageId: Int) : + this(UserError.Content(userMessageId), null) + object PublicationNotSearchable : + SearchUserError(R.string.search_error_not_searchable) + + class BadQuery(val error: Error) : + SearchUserError(R.string.search_error_not_searchable) + + class ResourceError(val error: Error) : + SearchUserError(R.string.search_error_other) + + class NetworkError(val error: Error) : + SearchUserError(R.string.search_error_other) + + object Cancelled : + SearchUserError(R.string.search_error_cancelled) + + class Other(val error: Error) : + SearchUserError(R.string.search_error_other) + + companion object { + + @OptIn(ExperimentalReadiumApi::class) + operator fun invoke(error: SearchError): SearchUserError = + when (error) { + is SearchError.BadQuery -> + BadQuery(error) + + SearchError.Cancelled -> + Cancelled + + is SearchError.NetworkError -> + NetworkError(error) + + is SearchError.Other -> + Other(error) + + SearchError.PublicationNotSearchable -> + PublicationNotSearchable + + is SearchError.ResourceError -> + ResourceError(error) + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/UserException.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/UserError.kt similarity index 56% rename from readium/shared/src/main/java/org/readium/r2/shared/UserException.kt rename to test-app/src/main/java/org/readium/r2/testapp/utils/UserError.kt index c7bb22f0f9..733598101a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/UserException.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/UserError.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared +package org.readium.r2.testapp.utils import android.content.Context import androidx.annotation.PluralsRes @@ -12,62 +12,37 @@ import androidx.annotation.StringRes import java.text.DateFormat import java.util.Date import org.joda.time.DateTime -import org.readium.r2.shared.extensions.asInstance /** * An exception that can be presented to the user using a localized message. */ -public open class UserException protected constructor( - protected val content: Content, - cause: Throwable? -) : Exception(cause) { +interface UserError { - public constructor(@StringRes userMessageId: Int, vararg args: Any?, cause: Throwable? = null) : - this(Content(userMessageId, *args), cause) + val content: Content - public constructor( - @PluralsRes userMessageId: Int, - quantity: Int?, - vararg args: Any?, - cause: Throwable? = null - ) : - this(Content(userMessageId, quantity, *args), cause) - - public constructor(message: String, cause: Throwable? = null) : - this(Content(message), cause) - - public constructor(cause: UserException) : - this(Content(cause), cause) - - /** - * Gets the localized user-facing message for this exception. - * - * @param includesCauses Includes nested [UserException] causes in the user message when true. - */ - public open fun getUserMessage(context: Context, includesCauses: Boolean = true): String = - content.getUserMessage(context, cause, includesCauses) + val cause: UserError? /** * Provides a way to generate a localized user message. */ - protected sealed class Content { + sealed class Content { - public abstract fun getUserMessage( + abstract fun getUserMessage( context: Context, - cause: Throwable? = null, + cause: UserError? = null, includesCauses: Boolean = true ): String /** - * Holds a nested [UserException]. + * Holds a nested [UserError]. */ - public class Exception(public val exception: UserException) : Content() { + class Error(val error: UserError) : Content() { override fun getUserMessage( context: Context, - cause: Throwable?, + cause: UserError?, includesCauses: Boolean ): String = - exception.getUserMessage(context, includesCauses) + error.getUserMessage(context, includesCauses) } /** @@ -77,14 +52,14 @@ public open class UserException protected constructor( * @param args Optional arguments to expand in the message. * @param quantity Quantity to use if the user message is a quantity strings. */ - public class LocalizedString( + class LocalizedString( private val userMessageId: Int, private val args: Array, private val quantity: Int? ) : Content() { override fun getUserMessage( context: Context, - cause: Throwable?, + cause: UserError?, includesCauses: Boolean ): String { // Convert complex objects to strings, such as Date, to be interpolated. @@ -107,10 +82,8 @@ public open class UserException protected constructor( context.getString(userMessageId, *(args.toTypedArray())) } - // Includes nested causes if they are also [UserException]. - val userException = cause?.asInstance() - if (userException != null && includesCauses) { - message += ": ${userException.getUserMessage(context, includesCauses)}" + if (cause != null && includesCauses) { + message += ": ${cause.getUserMessage(context, true)}" } return message @@ -121,27 +94,35 @@ public open class UserException protected constructor( * Holds an already localized string message. For example, received from an HTTP * Problem Details object. */ - public class Message(private val message: String) : Content() { + class Message(private val message: String) : Content() { override fun getUserMessage( context: Context, - cause: Throwable?, + cause: UserError?, includesCauses: Boolean ): String = message } - public companion object { - public operator fun invoke(@StringRes userMessageId: Int, vararg args: Any?): Content = + companion object { + operator fun invoke(@StringRes userMessageId: Int, vararg args: Any?): Content = LocalizedString(userMessageId, args, null) - public operator fun invoke( + operator fun invoke( @PluralsRes userMessageId: Int, quantity: Int?, vararg args: Any? ): Content = LocalizedString(userMessageId, args, quantity) - public operator fun invoke(cause: UserException): Content = - Exception(cause) - public operator fun invoke(message: String): Content = + operator fun invoke(cause: UserError): Content = + Error(cause) + operator fun invoke(message: String): Content = Message(message) } } } + +/** + * Gets the localized user-facing message for this exception. + * + * @param includesCauses Includes nested [UserError] causes in the user message when true. + */ +fun UserError.getUserMessage(context: Context, includesCauses: Boolean = true): String = + content.getUserMessage(context, cause, includesCauses) diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/ErrorExt.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/ErrorExt.kt index a2a097cdd8..2460082a85 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/ErrorExt.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/ErrorExt.kt @@ -7,9 +7,11 @@ package org.readium.r2.testapp.utils.extensions.readium import android.content.Context -import org.readium.r2.shared.UserException import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.testapp.utils.UserError +import org.readium.r2.testapp.utils.getUserMessage +import timber.log.Timber /** * Convenience function to get the description of an error with its cause. @@ -28,7 +30,7 @@ fun Error.toDebugDescription(context: Context): String = fun Throwable.toDebugDescription(context: Context): String { var desc = "${javaClass.nameWithEnclosingClasses()}: " - desc += (this as? UserException)?.getUserMessage(context) + desc += (this as? UserError)?.getUserMessage(context) ?: localizedMessage ?: message ?: "" desc += "\n" + stackTrace.take(2).joinToString("\n").prependIndent(" ") cause?.let { cause -> @@ -44,3 +46,28 @@ private fun Class<*>.nameWithEnclosingClasses(): String { } return name } + +// FIXME: to improve +fun Timber.Forest.e(error: Error, message: String? = null) { + e(Exception(error.message), message) +} + +fun Timber.Forest.w(error: Error, message: String? = null) { + w(Exception(error.message), message) +} + +/** + * Finds the first cause instance of the given type. + */ +inline fun Error.asInstance(): T? = + asInstance(T::class.java) + +/** + * Finds the first cause instance of the given type. + */ +fun Error.asInstance(klass: Class): R? = + @Suppress("UNCHECKED_CAST") + when { + klass.isInstance(this) -> this as R + else -> cause?.asInstance(klass) + } diff --git a/test-app/src/main/res/values/strings.xml b/test-app/src/main/res/values/strings.xml index e9506fc97c..c1d54eb444 100644 --- a/test-app/src/main/res/values/strings.xml +++ b/test-app/src/main/res/values/strings.xml @@ -116,6 +116,55 @@ Publication looks corrupted. An unexpected error occurred. + + + This interaction is not available + This License has a profile identifier that this app cannot handle, the publication cannot be processed + Can\'t retrieve the Certificate Revocation List + Network error + Unexpected LCP error + Unknown LCP error + + This license was cancelled on %1$s + This license has been returned on %1$s + This license starts on %1$s + This license expired on %1$s + + This license was revoked by its provider on %1$s. It was registered by %2$d device. + This license was revoked by its provider on %1$s. It was registered by %2$d devices. + + + Your publication could not be renewed properly + Incorrect renewal period, your publication could not be renewed + An unexpected error has occurred on the server + + Your publication could not be returned properly + Your publication has already been returned before or is expired + An unexpected error has occurred on the server + + The JSON is not representing a valid document + The JSON is malformed and can\'t be parsed + The JSON is not representing a valid License Document + The JSON is not representing a valid Status Document + + Can\'t open the license container + License not found in container + Can\'t read license from container + Can\'t write license in container + + Certificate has been revoked in the CRL + Certificate has not been signed by CA + License has been issued by an expired certificate + License signature does not match + User key check invalid + + Unable to decrypt encrypted content key from user key + Unable to decrypt encrypted content from content key + + Error + Could not open publication + Import error + Failed parsing Catalog No result Note From 6d5b5a3e1b724d1b304665fba50e0ee60138f57b Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 20 Nov 2023 10:55:08 +0100 Subject: [PATCH 15/86] Various changes --- .../adapter/pdfium/document/PdfiumDocument.kt | 8 +- .../navigator/PdfiumDocumentFragment.kt | 4 +- .../pspdfkit/document/ResourceDataProvider.kt | 4 +- .../readium/r2/lcp/LcpContentProtection.kt | 2 +- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 4 +- .../java/org/readium/r2/lcp/LcpLicense.kt | 4 +- .../readium/r2/shared/extensions/Exception.kt | 9 +++ .../AdeptFallbackContentProtection.kt | 8 +- .../LcpFallbackContentProtection.kt | 12 +-- .../iterators/HtmlResourceContentIterator.kt | 6 +- .../java/org/readium/r2/shared/util/Error.kt | 39 +++++++--- .../r2/shared/util/archive/ArchiveProvider.kt | 43 ++++++++--- .../util/archive/FileZipArchiveProvider.kt | 18 ++--- .../shared/util/archive/FileZipContainer.kt | 2 +- .../r2/shared/util/asset/AssetRetriever.kt | 55 ++++++-------- .../readium/r2/shared/util/asset/AssetType.kt | 24 ------ .../r2/shared/util/data/CompositeContainer.kt | 35 +++++++++ .../readium/r2/shared/util/data/Container.kt | 5 +- .../r2/shared/util/data/ContentBlob.kt | 10 ++- .../shared/util/data/ContentProviderError.kt | 2 +- .../readium/r2/shared/util/data/Decoding.kt | 38 +++++----- .../readium/r2/shared/util/data/FileBlob.kt | 2 +- .../r2/shared/util/data/InMemoryBlob.kt | 6 +- .../readium/r2/shared/util/data/ReadError.kt | 14 ---- .../r2/shared/util/data/RoutingContainer.kt | 58 --------------- .../r2/shared/util/http/DefaultHttpClient.kt | 9 ++- .../r2/shared/util/http/HttpResource.kt | 6 +- .../util/mediatype/MediaTypeRetriever.kt | 4 +- .../shared/util/mediatype/MediaTypeSniffer.kt | 74 +++++++++---------- .../util/resource/BlobResourceAdapters.kt | 8 +- .../util/resource/DirectoryContainer.kt | 11 ++- .../r2/shared/util/resource/Resource.kt | 7 +- ...ontainer.kt => SingleResourceContainer.kt} | 6 -- .../util/resource/TransformingContainer.kt | 12 +-- .../util/resource/TransformingResource.kt | 8 +- .../content/ResourceContentExtractor.kt | 4 +- .../r2/shared/util/zip/ChannelZipContainer.kt | 6 +- .../util/zip/StreamingZipArchiveProvider.kt | 18 ++--- .../readium/r2/streamer/ParserAssetFactory.kt | 12 +-- .../r2/streamer/parser/epub/EpubParser.kt | 4 +- .../parser/readium/LcpdfPositionsService.kt | 4 +- .../parser/readium/ReadiumWebPubParser.kt | 4 +- .../org/readium/r2/testapp/MainActivity.kt | 4 +- .../java/org/readium/r2/testapp/Readium.kt | 5 +- .../r2/testapp/bookshelf/BookshelfFragment.kt | 4 +- .../r2/testapp/domain/PublicationError.kt | 2 +- .../r2/testapp/drm/DrmManagementFragment.kt | 4 +- .../r2/testapp/reader/ReaderViewModel.kt | 6 +- .../r2/testapp/reader/VisualReaderFragment.kt | 4 +- .../utils/extensions/readium/ErrorExt.kt | 73 ------------------ 50 files changed, 320 insertions(+), 391 deletions(-) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetType.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/data/CompositeContainer.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingContainer.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/resource/{ResourceContainer.kt => SingleResourceContainer.kt} (81%) delete mode 100644 test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/ErrorExt.kt diff --git a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt index 0c218dcd43..05914dbca3 100644 --- a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt +++ b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt @@ -18,10 +18,10 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.extensions.md5 import org.readium.r2.shared.extensions.tryOrNull +import org.readium.r2.shared.extensions.unwrapInstance import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException -import org.readium.r2.shared.util.data.unwrapReadException import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory @@ -114,7 +114,11 @@ public class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory exception.error else -> ReadError.Decoding("Pdfium could not read data.") } diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt index 1e34caccd8..4a47ea7b13 100644 --- a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumDocumentFragment.kt @@ -27,8 +27,8 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.SingleJob import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.e import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.toDebugDescription import timber.log.Timber @ExperimentalReadiumApi @@ -76,7 +76,7 @@ public class PdfiumDocumentFragment internal constructor( // .cachedIn(publication) .open(resource, null) .getOrElse { error -> - Timber.e(error) + Timber.e(error.toDebugDescription()) listener?.onResourceLoadFailed(href, error) return@launch } diff --git a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/ResourceDataProvider.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/ResourceDataProvider.kt index 04395129d5..65e881f429 100644 --- a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/ResourceDataProvider.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/ResourceDataProvider.kt @@ -10,16 +10,16 @@ import com.pspdfkit.document.providers.DataProvider import java.util.UUID import kotlinx.coroutines.runBlocking import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.e import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.isLazyInitialized import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.synchronized +import org.readium.r2.shared.util.toDebugDescription import timber.log.Timber internal class ResourceDataProvider( resource: Resource, - private val onResourceError: (ReadError) -> Unit = { Timber.e(it) } + private val onResourceError: (ReadError) -> Unit = { Timber.e(it.toDebugDescription()) } ) : DataProvider { var error: ReadError? = null diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index 3b86f4247f..cae810f317 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -178,7 +178,7 @@ internal class LcpContentProtection( when (this) { is AssetRetriever.Error.ArchiveFormatNotSupported -> ContentProtection.Error.UnsupportedAsset(this) - is AssetRetriever.Error.AccessError -> + is AssetRetriever.Error.ReadError -> ContentProtection.Error.ReadError(cause) is AssetRetriever.Error.SchemeNotSupported -> ContentProtection.Error.UnsupportedAsset(this) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 03501f6c5b..b215e7c374 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -103,7 +103,7 @@ internal class LcpDecryptor( ) .tryRecover { error -> when (error) { - is MediaTypeSnifferError.DataAccess -> + is MediaTypeSnifferError.Read -> Try.failure(error.cause) MediaTypeSnifferError.NotRecognized -> Try.success(MediaType.BINARY) @@ -158,7 +158,7 @@ internal class LcpDecryptor( blob = this ).tryRecover { error -> when (error) { - is MediaTypeSnifferError.DataAccess -> + is MediaTypeSnifferError.Read -> Try.failure(error.cause) MediaTypeSnifferError.NotRecognized -> Try.success(MediaType.BINARY) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt index 03ab20ec71..1ac13e10a1 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpLicense.kt @@ -20,7 +20,7 @@ import org.readium.r2.shared.publication.services.ContentProtectionService import org.readium.r2.shared.util.Closeable import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.e +import org.readium.r2.shared.util.toDebugDescription import timber.log.Timber /** @@ -124,7 +124,7 @@ public interface LcpLicense : ContentProtectionService.UserRights, Closeable { ) public fun decipher(data: ByteArray): ByteArray? = runBlocking { decrypt(data) } - .onFailure { Timber.e(it) } + .onFailure { Timber.e(it.toDebugDescription()) } .getOrNull() @Deprecated( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Exception.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Exception.kt index a2bee64dfb..cad1fc49b5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Exception.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Exception.kt @@ -54,3 +54,12 @@ public fun Throwable.asInstance(klass: Class): R? = klass.isInstance(this) -> this as R else -> cause?.asInstance(klass) } + +/** + * Unwraps the nearest instance of [klass] if any. + */ +@InternalReadiumApi +public fun Exception.unwrapInstance(klass: Class): Exception { + asInstance(klass)?.let { return it } + return this +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt index f5b50d9c4f..fb6e535313 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt @@ -70,9 +70,9 @@ public class AdeptFallbackContentProtection : ContentProtection { ?.readAsXml() ?.getOrElse { when (it) { - is DecoderError.DecodingError -> + is DecoderError.Decoding -> return Try.success(false) - is DecoderError.DataAccess -> + is DecoderError.Read -> return Try.failure(it.cause) } }?.get("EncryptedData", EpubEncryption.ENC) @@ -85,9 +85,9 @@ public class AdeptFallbackContentProtection : ContentProtection { ?.readAsXml() ?.getOrElse { when (it) { - is DecoderError.DecodingError -> + is DecoderError.Decoding -> return Try.success(false) - is DecoderError.DataAccess -> + is DecoderError.Read -> return Try.failure(it.cause) } }?.takeIf { it.namespace == "http://ns.adobe.com/adept" } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index f5a86fa5a6..6ae06a687c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -85,9 +85,9 @@ public class LcpFallbackContentProtection : ContentProtection { ?.readAsJson() ?.getOrElse { when (it) { - is DecoderError.DataAccess -> + is DecoderError.Read -> Try.failure(it.cause.cause) - is DecoderError.DecodingError -> + is DecoderError.Decoding -> return Try.success(false) } } @@ -103,9 +103,9 @@ public class LcpFallbackContentProtection : ContentProtection { ?.readAsRwpm() ?.getOrElse { when (it) { - is DecoderError.DataAccess -> + is DecoderError.Read -> return Try.failure(ReadError.Decoding(it)) - is DecoderError.DecodingError -> + is DecoderError.Decoding -> return Try.success(false) } } @@ -124,9 +124,9 @@ public class LcpFallbackContentProtection : ContentProtection { ?.readAsXml() ?.getOrElse { when (it) { - is DecoderError.DataAccess -> + is DecoderError.Read -> return Try.failure(ReadError.Decoding(it.cause.cause)) - is DecoderError.DecodingError -> + is DecoderError.Decoding -> return Try.failure(ReadError.Decoding(it.cause)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt index 486215a17b..eda7daa799 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt @@ -32,13 +32,14 @@ import org.readium.r2.shared.publication.services.content.Content.TextElement import org.readium.r2.shared.publication.services.content.Content.VideoElement import org.readium.r2.shared.publication.services.positionsByReadingOrder import org.readium.r2.shared.util.Language +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.readAsString import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.toDebugDescription import org.readium.r2.shared.util.use -import org.readium.r2.shared.util.w import timber.log.Timber /** @@ -154,7 +155,8 @@ public class HtmlResourceContentIterator internal constructor( withContext(Dispatchers.Default) { val document = resource.use { res -> val html = res.readAsString().getOrElse { - Timber.w(it, "Failed to read HTML resource") + val error = MessageError("Failed to read HTML resource", it.cause) + Timber.w(error.toDebugDescription()) return@withContext ParsedElements() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt index e2b567d648..fd50762d98 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt @@ -6,9 +6,6 @@ package org.readium.r2.shared.util -import org.readium.r2.shared.InternalReadiumApi -import timber.log.Timber - /** * Describes an error. */ @@ -50,13 +47,35 @@ public class ErrorException( public val error: Error ) : Exception(error.message, error.cause?.let { ErrorException(it) }) -// FIXME: to improve -@InternalReadiumApi -public fun Timber.Forest.e(error: Error, message: String? = null) { - e(Exception(error.message), message) +/** + * Convenience function to get the description of an error with its cause. + */ +public fun Error.toDebugDescription(): String = + if (this is ThrowableError<*>) { + throwable.toDebugDescription() + } else { + var desc = "${javaClass.nameWithEnclosingClasses()}: $message" + cause?.let { cause -> + desc += "\n\n${cause.toDebugDescription()}" + } + desc + } + +private fun Throwable.toDebugDescription(): String { + var desc = "${javaClass.nameWithEnclosingClasses()}: " + + desc += message ?: "" + desc += "\n" + stackTrace.take(2).joinToString("\n").prependIndent(" ") + cause?.let { cause -> + desc += "\n\n${cause.toDebugDescription()}" + } + return desc } -@InternalReadiumApi -public fun Timber.Forest.w(error: Error, message: String? = null) { - w(Exception(error.message), message) +private fun Class<*>.nameWithEnclosingClasses(): String { + var name = simpleName + enclosingClass?.let { + name = "${it.nameWithEnclosingClasses()}.$name" + } + return name } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt index 81f552e36b..5f86157a7d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt @@ -10,14 +10,37 @@ import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.mediatype.CompositeMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceContainer public interface ArchiveProvider : MediaTypeSniffer, ArchiveFactory +public class CompositeArchiveProvider( + providers: List +) : ArchiveProvider { + + private val archiveFactory = CompositeArchiveFactory(providers) + + private val mediaTypeSniffer = CompositeMediaTypeSniffer(providers) + + override fun sniffHints(hints: MediaTypeHints): Try = + mediaTypeSniffer.sniffHints(hints) + + override suspend fun sniffBlob(blob: Blob): Try = + mediaTypeSniffer.sniffBlob(blob) + override suspend fun create( + blob: Blob, + password: String? + ): Try, ArchiveFactory.Error> = + archiveFactory.create(blob, password) +} + /** * A factory to create a [ResourceContainer]s from archive [Blob]s. * @@ -36,20 +59,20 @@ public interface ArchiveFactory { public constructor(exception: Exception) : this(ThrowableError(exception)) } - public class UnsupportedFormat( + public class FormatNotSupported( cause: org.readium.r2.shared.util.Error? = null ) : Error("Resource is not supported.", cause) - public class ResourceError( - override val cause: ReadError + public class ReadError( + override val cause: org.readium.r2.shared.util.data.ReadError ) : Error("An error occurred while attempting to read the resource.", cause) } /** - * Creates a new archive [ResourceContainer] to access the entries of the given archive. + * Creates a new [Container] to access the entries of the given archive. */ public suspend fun create( - resource: Blob, + blob: Blob, password: String? = null ): Try, Error> } @@ -61,20 +84,20 @@ public class CompositeArchiveFactory( public constructor(vararg factories: ArchiveFactory) : this(factories.toList()) override suspend fun create( - resource: Blob, + blob: Blob, password: String? ): Try, ArchiveFactory.Error> { for (factory in factories) { - factory.create(resource, password) + factory.create(blob, password) .getOrElse { error -> when (error) { - is ArchiveFactory.Error.UnsupportedFormat -> null + is ArchiveFactory.Error.FormatNotSupported -> null else -> return Try.failure(error) } } ?.let { return Try.success(it) } } - return Try.failure(ArchiveFactory.Error.UnsupportedFormat()) + return Try.failure(ArchiveFactory.Error.FormatNotSupported()) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt index a84f9d6a55..4c04c71e8a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt @@ -55,13 +55,13 @@ public class FileZipArchiveProvider( Try.failure(MediaTypeSnifferError.NotRecognized) } catch (e: SecurityException) { Try.failure( - MediaTypeSnifferError.DataAccess( + MediaTypeSnifferError.Read( ReadError.Access(FileSystemError.Forbidden(e)) ) ) } catch (e: IOException) { Try.failure( - MediaTypeSnifferError.DataAccess( + MediaTypeSnifferError.Read( ReadError.Access(FileSystemError.IO(e)) ) ) @@ -70,16 +70,16 @@ public class FileZipArchiveProvider( } override suspend fun create( - resource: Blob, + blob: Blob, password: String? ): Try, ArchiveFactory.Error> { if (password != null) { return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) } - val file = resource.source?.toFile() + val file = blob.source?.toFile() ?: return Try.Failure( - ArchiveFactory.Error.UnsupportedFormat( + ArchiveFactory.Error.FormatNotSupported( MessageError("Resource not supported because file cannot be directly accessed.") ) ) @@ -98,25 +98,25 @@ public class FileZipArchiveProvider( Try.success(archive) } catch (e: FileNotFoundException) { Try.failure( - ArchiveFactory.Error.ResourceError( + ArchiveFactory.Error.ReadError( ReadError.Access(FileSystemError.NotFound(e)) ) ) } catch (e: ZipException) { Try.failure( - ArchiveFactory.Error.ResourceError( + ArchiveFactory.Error.ReadError( ReadError.Decoding(e) ) ) } catch (e: SecurityException) { Try.failure( - ArchiveFactory.Error.ResourceError( + ArchiveFactory.Error.ReadError( ReadError.Access(FileSystemError.Forbidden(e)) ) ) } catch (e: IOException) { Try.failure( - ArchiveFactory.Error.ResourceError( + ArchiveFactory.Error.ReadError( ReadError.Access(FileSystemError.IO(e)) ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt index 02f70d9cb4..f5d618e8d6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt @@ -50,7 +50,7 @@ internal class FileZipContainer( blob = this ).tryRecover { error -> when (error) { - is MediaTypeSnifferError.DataAccess -> + is MediaTypeSnifferError.Read -> Try.failure(error.cause) MediaTypeSnifferError.NotRecognized -> Try.success(MediaType.BINARY) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index d25c72ede8..82fa4189f4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -7,21 +7,16 @@ package org.readium.r2.shared.util.asset import java.io.File -import kotlin.Exception -import kotlin.String import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.archive.ArchiveProvider -import org.readium.r2.shared.util.archive.CompositeArchiveFactory import org.readium.r2.shared.util.archive.FileZipArchiveProvider -import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.mediatype.CompositeMediaTypeSniffer import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.toUrl @@ -32,13 +27,8 @@ import org.readium.r2.shared.util.toUrl */ public class AssetRetriever( private val resourceFactory: ResourceFactory = FileResourceFactory(), - archiveProviders: List = listOf(FileZipArchiveProvider()) + private val archiveProvider: ArchiveProvider = FileZipArchiveProvider() ) { - private val archiveSniffer: MediaTypeSniffer = - CompositeMediaTypeSniffer(archiveProviders) - - private val archiveFactory: ArchiveFactory = - CompositeArchiveFactory(archiveProviders) public sealed class Error( override val message: String, @@ -48,20 +38,12 @@ public class AssetRetriever( public class SchemeNotSupported( public val scheme: Url.Scheme, cause: org.readium.r2.shared.util.Error? = null - ) : Error("Scheme $scheme is not supported.", cause) { - - public constructor(scheme: Url.Scheme, exception: Exception) : - this(scheme, ThrowableError(exception)) - } + ) : Error("Scheme $scheme is not supported.", cause) - public class ArchiveFormatNotSupported(cause: org.readium.r2.shared.util.Error?) : - Error("Archive factory does not support this kind of archive.", cause) { - - public constructor(exception: Exception) : - this(ThrowableError(exception)) - } + public class ArchiveFormatNotSupported(cause: org.readium.r2.shared.util.Error) : + Error("Archive providers do not support this kind of archive.", cause) - public class AccessError(override val cause: ReadError) : + public class ReadError(override val cause: org.readium.r2.shared.util.data.ReadError) : Error("An error occurred when trying to read asset.", cause) } @@ -97,11 +79,20 @@ public class AssetRetriever( mediaType: MediaType, containerType: MediaType ): Try { - val container = archiveFactory.create(resource) + archiveProvider.sniffHints(MediaTypeHints(mediaType = containerType)) + .onFailure { + return Try.failure( + Error.ArchiveFormatNotSupported( + MessageError("Container type $containerType not recognized.") + ) + ) + } + + val container = archiveProvider.create(resource) .mapFailure { error -> when (error) { - is ArchiveFactory.Error.ResourceError -> - Error.AccessError(error.cause) + is ArchiveFactory.Error.ReadError -> + Error.ReadError(error.cause) else -> Error.ArchiveFormatNotSupported(error) } @@ -166,9 +157,9 @@ public class AssetRetriever( } val mediaType = resource.mediaType() - .getOrElse { return Try.failure(Error.AccessError(it)) } + .getOrElse { return Try.failure(Error.ReadError(it)) } - return archiveSniffer.sniffBlob(resource) + return archiveProvider.sniffBlob(resource) .fold( { containerType -> retrieveArchiveAsset(url, mediaType = mediaType, containerType = containerType) @@ -177,8 +168,8 @@ public class AssetRetriever( when (error) { MediaTypeSnifferError.NotRecognized -> Try.success(Asset.Resource(mediaType, resource)) - is MediaTypeSnifferError.DataAccess -> - Try.failure(Error.AccessError(error.cause)) + is MediaTypeSnifferError.Read -> + Try.failure(Error.ReadError(error.cause)) } } ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetType.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetType.kt deleted file mode 100644 index 852f5d7c35..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetType.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.asset - -import org.readium.r2.shared.util.MapCompanion - -public enum class AssetType(public val value: String) { - - /** - * A simple resource. - */ - Resource("resource"), - - /** - * An archive container. - */ - Archive("archive"); - - public companion object : MapCompanion(values(), AssetType::value) -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/CompositeContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/CompositeContainer.kt new file mode 100644 index 0000000000..6166e41399 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/CompositeContainer.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import org.readium.r2.shared.util.Url + +/** + * Routes requests to child containers, depending on a provided predicate. + * + * This can be used for example to serve a publication containing both local and remote resources, + * and more generally to concatenate different content sources. + * + * The [containers] will be tested in the given order. + */ +public class CompositeContainer( + private val containers: List> +) : Container { + + public constructor(vararg containers: Container) : + this(containers.toList()) + + override val entries: Set = + containers.fold(emptySet()) { acc, container -> acc + container.entries } + + override fun get(url: Url): E? = + containers.firstNotNullOfOrNull { it[url] } + + override suspend fun close() { + containers.forEach { it.close() } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index 37076c58f8..a200a4cf04 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -30,10 +30,7 @@ public interface Container : Iterable, SuspendingCloseable { entries.iterator() /** - * Returns the [Entry] at the given [url]. - * - * A [Entry] is always returned, since for some cases we can't know if it exists before actually - * fetching it, such as HTTP. Therefore, errors are handled at the Entry level. + * Returns the entry at the given [url] or null if there is none. */ public operator fun get(url: Url): E? } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt index c2a114a22d..c3873f5599 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt @@ -22,7 +22,7 @@ import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.toUrl /** - * A [Resource] to access content [uri] thanks to a [ContentResolver]. + * A [Blob] to access content [uri] thanks to a [ContentResolver]. */ public class ContentBlob( private val uri: Uri, @@ -64,7 +64,7 @@ public class ContentBlob( while (skipped != range.first) { skipped += it.skip(range.first - skipped) if (skipped == 0L) { - throw IOException("Could not skip InputStream.") + throw IOException("Could not skip InputStream to read ranges from $uri.") } } @@ -97,8 +97,10 @@ public class ContentBlob( return Try.catching { val stream = contentResolver.openInputStream(uri) ?: return Try.failure( - ReadError.Other( - Exception("Content provider recently crashed.") + ReadError.Access( + ContentProviderError.NotAvailable( + MessageError("Content provider recently crashed.") + ) ) ) val result = block(stream) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt index d1fa05f965..3939afc2f0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt @@ -29,7 +29,7 @@ public sealed class ContentProviderError( } public class IO( - cause: Error? + override val cause: Error ) : ContentProviderError("An IO error occurred.", cause) { public constructor(exception: Exception) : this(ThrowableError(exception)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index ed9ac249f2..4ba5fecb47 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -27,11 +27,11 @@ public sealed class DecoderError( override val message: String ) : Error { - public class DataAccess( + public class Read( override val cause: ReadError - ) : DecoderError("Data source error") + ) : DecoderError("Reading error") - public class DecodingError( + public class Decoding( override val cause: Error? ) : DecoderError("Decoding Error") } @@ -43,16 +43,16 @@ internal suspend fun Try.decode( when (this) { is Try.Success -> try { - Try.success( - withContext(Dispatchers.Default) { - block(value) - } - ) + withContext(Dispatchers.Default) { + Try.success(block(value)) + } } catch (e: Exception) { - Try.failure(DecoderError.DecodingError(wrapError(e))) + Try.failure(DecoderError.Decoding(wrapError(e))) + } catch (e: OutOfMemoryError) { + Try.failure(DecoderError.Read(ReadError.OutOfMemory(e))) } is Try.Failure -> - Try.failure(DecoderError.DataAccess(value)) + Try.failure(DecoderError.Read(value)) } internal suspend fun Try.decodeMap( @@ -62,13 +62,13 @@ internal suspend fun Try.decodeMap( when (this) { is Try.Success -> try { - Try.success( - withContext(Dispatchers.Default) { - block(value) - } - ) + withContext(Dispatchers.Default) { + Try.success(block(value)) + } } catch (e: Exception) { - Try.failure(DecoderError.DecodingError(wrapError(e))) + Try.failure(DecoderError.Decoding(wrapError(e))) + } catch (e: OutOfMemoryError) { + Try.failure(DecoderError.Read(ReadError.OutOfMemory(e))) } is Try.Failure -> Try.failure(value) @@ -110,7 +110,7 @@ public suspend fun Blob.readAsRwpm(): Try = Manifest.fromJSON(json) ?.let { Try.success(it) } ?: Try.failure( - DecoderError.DecodingError( + DecoderError.Decoding( MessageError("Content is not a valid RWPM.") ) ) @@ -121,12 +121,12 @@ public suspend fun Blob.readAsRwpm(): Try = */ public suspend fun Blob.readAsBitmap(): Try = read() - .mapFailure { DecoderError.DataAccess(it) } + .mapFailure { DecoderError.Read(it) } .flatMap { bytes -> BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?.let { Try.success(it) } ?: Try.failure( - DecoderError.DecodingError( + DecoderError.Decoding( MessageError("Could not decode resource as a bitmap.") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt index 43b393773e..8af2eaecbf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt @@ -22,7 +22,7 @@ import org.readium.r2.shared.util.isLazyInitialized import org.readium.r2.shared.util.toUrl /** - * A [Resource] to access a [file]. + * A [Blob] to access a [File]. */ public class FileBlob( private val file: File diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/InMemoryBlob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/InMemoryBlob.kt index 1e79db7f32..8c661a44ee 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/InMemoryBlob.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/InMemoryBlob.kt @@ -13,7 +13,7 @@ import org.readium.r2.shared.extensions.requireLengthFitInt import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -/** Creates a Blob serving a [ByteArray]. */ +/** Creates a [Blob] serving a [ByteArray]. */ public class InMemoryBlob( override val source: AbsoluteUrl?, private val bytes: suspend () -> Try @@ -24,11 +24,11 @@ public class InMemoryBlob( source: AbsoluteUrl? = null ) : this(source = source, { Try.success(bytes) }) + private lateinit var _bytes: Try + override suspend fun length(): Try = read().map { it.size.toLong() } - private lateinit var _bytes: Try - override suspend fun read(range: LongRange?): Try { if (!::_bytes.isInitialized) { _bytes = bytes() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt index dcc6f3218e..3a8da2f3f9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt @@ -7,7 +7,6 @@ package org.readium.r2.shared.util.data import java.io.IOException -import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.MessageError @@ -55,16 +54,3 @@ public sealed class ReadError( public class ReadException( public val error: ReadError ) : IOException(error.message, ErrorException(error)) - -@InternalReadiumApi -public fun Exception.unwrapReadException(): Exception { - fun Throwable.findReadExceptionCause(): ReadException? = - when { - this is ReadException -> this - cause != null -> cause!!.findReadExceptionCause() - else -> null - } - - this.findReadExceptionCause()?.let { return it } - return this -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingContainer.kt deleted file mode 100644 index f58eed51a3..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/RoutingContainer.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.data - -import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.Url - -/** - * Routes requests to child containers, depending on a provided predicate. - * - * This can be used for example to serve a publication containing both local and remote resources, - * and more generally to concatenate different content sources. - * - * The [routes] will be tested in the given order. - */ -public class RoutingContainer( - private val routes: List> -) : Container { - - /** - * Holds a child fetcher and the predicate used to determine if it can answer a request. - * - * The default value for [accepts] means that the fetcher will accept any link. - */ - public class Route( - public val container: Container, - public val accepts: (Url) -> Boolean = { true } - ) - - public constructor(local: Container, remote: Container) : - this( - listOf( - Route(local, accepts = ::isLocal), - Route(remote) - ) - ) - - override val entries: Set = - routes.fold(emptySet()) { acc, route -> acc + route.container.entries } - - override fun get(url: Url): E? = - routes.firstOrNull { it.accepts(url) }?.container?.get(url) - - override suspend fun close() { - routes.forEach { it.container.close() } - } -} - -private fun isLocal(url: Url): Boolean { - if (url !is AbsoluteUrl) { - return true - } - return !url.isHttp -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index 7079b32a14..5093b11d35 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -12,8 +12,10 @@ import java.io.FileInputStream import java.io.IOException import java.io.InputStream import java.net.HttpURLConnection +import java.net.NoRouteToHostException import java.net.SocketTimeoutException import java.net.URL +import java.net.UnknownHostException import kotlin.time.Duration import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -25,13 +27,13 @@ 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.data.InMemoryBlob -import org.readium.r2.shared.util.e import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrDefault import org.readium.r2.shared.util.http.HttpRequest.Method 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 org.readium.r2.shared.util.toDebugDescription import org.readium.r2.shared.util.tryRecover import timber.log.Timber @@ -224,7 +226,8 @@ public class DefaultHttpClient( } .onFailure { callback.onRequestFailed(request, it) - Timber.e(it, "HTTP request failed ${request.url}") + val error = MessageError("HTTP request failed ${request.url}", it) + Timber.e(error.toDebugDescription()) } } @@ -336,6 +339,8 @@ public class DefaultHttpClient( */ private fun wrap(cause: IOException): HttpError = when (cause) { + is UnknownHostException, is NoRouteToHostException -> + HttpError.UnreachableHost(ThrowableError(cause)) is SocketTimeoutException -> HttpError.Timeout(ThrowableError(cause)) else -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index bd24b8220d..7eb34c6105 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -16,7 +16,7 @@ import org.readium.r2.shared.util.io.CountingInputStream import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource -/** Provides access to an external URL. */ +/** Provides access to an external URL through HTTP. */ @OptIn(ExperimentalReadiumApi::class) public class HttpResource( private val client: HttpClient, @@ -106,7 +106,9 @@ public class HttpResource( .flatMap { response -> if (from != null && response.response.statusCode != 206 ) { - val error = MessageError("Server seems not to support range requests.") + val error = MessageError( + "Server seems not to support range requests to $source." + ) Try.failure(ReadError.UnsupportedOperation(error)) } else { Try.success(response) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt index 22c0de6d39..367ab46e25 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt @@ -20,8 +20,8 @@ import org.readium.r2.shared.util.toUri * Retrieves a canonical [MediaType] for the provided media type and file extension hints and/or * asset content. * - * The actual format sniffing is done by the provided [sniffers]. The [defaultSniffers] cover the - * formats supported with Readium by default. + * The actual format sniffing is mostly done by the provided [mediaTypeSniffer]. + * The [DefaultMediaTypeSniffer] cover the formats supported with Readium by default. */ public class MediaTypeRetriever( private val contentResolver: ContentResolver? = null, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index 0e90127c2e..b2cfcc4b34 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -40,7 +40,7 @@ public sealed class MediaTypeSnifferError( public data object NotRecognized : MediaTypeSnifferError("Media type of resource could not be inferred.", null) - public data class DataAccess(override val cause: ReadError) : + public data class Read(override val cause: ReadError) : MediaTypeSnifferError("An error occurred while trying to read content.", cause) } public interface HintMediaTypeSniffer { @@ -94,7 +94,7 @@ public interface MediaTypeSniffer : Try.failure(MediaTypeSnifferError.NotRecognized) } -internal open class CompositeMediaTypeSniffer( +internal class CompositeMediaTypeSniffer( private val sniffers: List ) : MediaTypeSniffer { @@ -164,11 +164,11 @@ public class XhtmlMediaTypeSniffer : MediaTypeSniffer { blob.readAsXml() .getOrElse { when (it) { - is DecoderError.DataAccess -> + is DecoderError.Read -> return Try.failure( - MediaTypeSnifferError.DataAccess(it.cause) + MediaTypeSnifferError.Read(it.cause) ) - is DecoderError.DecodingError -> + is DecoderError.Decoding -> null } } @@ -201,9 +201,9 @@ public class HtmlMediaTypeSniffer : MediaTypeSniffer { blob.readAsXml() .getOrElse { when (it) { - is DecoderError.DataAccess -> - return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) - is DecoderError.DecodingError -> + is DecoderError.Read -> + return Try.failure(MediaTypeSnifferError.Read(it.cause)) + is DecoderError.Decoding -> null } } @@ -213,10 +213,10 @@ public class HtmlMediaTypeSniffer : MediaTypeSniffer { blob.readAsString() .getOrElse { when (it) { - is DecoderError.DataAccess -> - return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) + is DecoderError.Read -> + return Try.failure(MediaTypeSnifferError.Read(it.cause)) - is DecoderError.DecodingError -> + is DecoderError.Decoding -> null } } @@ -269,9 +269,9 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { blob.readAsXml() .getOrElse { when (it) { - is DecoderError.DataAccess -> - return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) - is DecoderError.DecodingError -> + is DecoderError.Read -> + return Try.failure(MediaTypeSnifferError.Read(it.cause)) + is DecoderError.Decoding -> null } } @@ -288,9 +288,9 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { blob.readAsRwpm() .getOrElse { when (it) { - is DecoderError.DataAccess -> - return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) - is DecoderError.DecodingError -> + is DecoderError.Read -> + return Try.failure(MediaTypeSnifferError.Read(it.cause)) + is DecoderError.Decoding -> null } } @@ -320,10 +320,10 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { blob.containsJsonKeys("id", "title", "authentication") .getOrElse { when (it) { - is DecoderError.DataAccess -> - return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) + is DecoderError.Read -> + return Try.failure(MediaTypeSnifferError.Read(it.cause)) - is DecoderError.DecodingError -> + is DecoderError.Decoding -> null } } @@ -351,10 +351,10 @@ public object LcpLicenseMediaTypeSniffer : MediaTypeSniffer { blob.containsJsonKeys("id", "issued", "provider", "encryption") .getOrElse { when (it) { - is DecoderError.DataAccess -> - return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) + is DecoderError.Read -> + return Try.failure(MediaTypeSnifferError.Read(it.cause)) - is DecoderError.DecodingError -> + is DecoderError.Decoding -> null } } @@ -443,10 +443,10 @@ public class WebPubManifestMediaTypeSniffer : MediaTypeSniffer { blob.readAsRwpm() .getOrElse { when (it) { - is DecoderError.DataAccess -> - return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) + is DecoderError.Read -> + return Try.failure(MediaTypeSnifferError.Read(it.cause)) - is DecoderError.DecodingError -> + is DecoderError.Decoding -> null } } @@ -513,7 +513,7 @@ public class WebPubMediaTypeSniffer : MediaTypeSniffer { container[RelativeUrl("manifest.json")!!] ?.read() ?.getOrElse { error -> - return Try.failure(MediaTypeSnifferError.DataAccess(error)) + return Try.failure(MediaTypeSnifferError.Read(error)) } ?.let { tryOrNull { Manifest.fromJSON(JSONObject(String(it))) } } ?: return Try.failure(MediaTypeSnifferError.NotRecognized) @@ -548,10 +548,10 @@ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { val string = blob.readAsString() .getOrElse { when (it) { - is DecoderError.DataAccess -> - return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) + is DecoderError.Read -> + return Try.failure(MediaTypeSnifferError.Read(it.cause)) - is DecoderError.DecodingError -> + is DecoderError.Decoding -> null } } ?: "" @@ -587,7 +587,7 @@ public class EpubMediaTypeSniffer : MediaTypeSniffer { val mimetype = container[RelativeUrl("mimetype")!!] ?.read() ?.getOrElse { error -> - return Try.failure(MediaTypeSnifferError.DataAccess(error)) + return Try.failure(MediaTypeSnifferError.Read(error)) } ?.let { String(it, charset = Charsets.US_ASCII).trim() } if (mimetype == "application/epub+zip") { @@ -626,7 +626,7 @@ public object LpfMediaTypeSniffer : MediaTypeSniffer { container[RelativeUrl("publication.json")!!] ?.read() ?.getOrElse { error -> - return Try.failure(MediaTypeSnifferError.DataAccess(error)) + return Try.failure(MediaTypeSnifferError.Read(error)) } ?.let { tryOrNull { String(it) } } ?.let { manifest -> @@ -754,7 +754,7 @@ public object PdfMediaTypeSniffer : MediaTypeSniffer { override suspend fun sniffBlob(blob: Blob): Try { blob.read(0L until 5L) .getOrElse { error -> - return Try.failure(MediaTypeSnifferError.DataAccess(error)) + return Try.failure(MediaTypeSnifferError.Read(error)) } .let { tryOrNull { it.toString(Charsets.UTF_8) } } .takeIf { it == "%PDF-" } @@ -778,10 +778,10 @@ public object JsonMediaTypeSniffer : MediaTypeSniffer { blob.readAsJson() .getOrElse { when (it) { - is DecoderError.DataAccess -> - return Try.failure(MediaTypeSnifferError.DataAccess(it.cause)) + is DecoderError.Read -> + return Try.failure(MediaTypeSnifferError.Read(it.cause)) - is DecoderError.DecodingError -> + is DecoderError.Decoding -> null } } @@ -825,7 +825,7 @@ public class SystemMediaTypeSniffer : MediaTypeSniffer { e.findSystemSnifferException() ?.let { return Try.failure( - MediaTypeSnifferError.DataAccess(it.error) + MediaTypeSnifferError.Read(it.error) ) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt index d04e373f37..ef14ffc0b5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt @@ -1,3 +1,9 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.Try @@ -34,7 +40,7 @@ internal class GuessMediaTypeResourceAdapter( blob = blob ).tryRecover { error -> when (error) { - is MediaTypeSnifferError.DataAccess -> + is MediaTypeSnifferError.Read -> Try.failure(error.cause) MediaTypeSnifferError.NotRecognized -> Try.success(MediaType.BINARY) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index 660d9ea748..53a58f43f0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -9,7 +9,6 @@ package org.readium.r2.shared.util.resource import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.readium.r2.shared.extensions.isParentOf import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url @@ -37,11 +36,11 @@ public class DirectoryContainer( ) } - override fun get(url: Url): Resource? = - (url as? RelativeUrl)?.path - ?.let { File(root, it) } - ?.takeIf { !root.isParentOf(it) } - ?.toResource() + override fun get(url: Url): Resource? = url + .takeIf { it in entries } + ?.let { (it as? RelativeUrl)?.path } + ?.let { File(root, it) } + ?.toResource() override suspend fun close() {} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index f3b841ceb6..c10cd86a7a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -9,9 +9,14 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType +public typealias ResourceTry = Try + +public typealias ResourceContainer = Container + /** * Acts as a proxy to an actual resource by handling read access. */ @@ -63,7 +68,7 @@ public class FailureResource( } @Deprecated( - "Catch exceptions yourself to the most suitable ResourceError.", + "Catch exceptions yourself to the most suitable ReadError.", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("map(transform)") ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt similarity index 81% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt index 1001a90fea..cd1cdff5ec 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt @@ -6,14 +6,8 @@ package org.readium.r2.shared.util.resource -import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.ReadError - -public typealias ResourceTry = Try - -public typealias ResourceContainer = Container /** A [Container] for a single [Resource]. */ public class SingleResourceContainer( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt index ab17304603..058d2f9c41 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingContainer.kt @@ -16,29 +16,29 @@ import org.readium.r2.shared.util.data.Container * * If the transformation doesn't apply, simply return the resource unchanged. */ -public typealias ResourceTransformer = (Resource) -> Resource +public typealias EntryTransformer = (Url, Resource) -> Resource /** - * Transforms the resources' content of a child fetcher using a list of [ResourceTransformer] + * Transforms the resources' content of a child fetcher using a list of [EntryTransformer] * functions. */ public class TransformingContainer( private val container: Container, - private val transformers: List<(Url, Resource) -> Resource> + private val transformers: List ) : Container { - public constructor(container: Container, transformer: (Url, Resource) -> Resource) : + public constructor(container: Container, transformer: EntryTransformer) : this(container, listOf(transformer)) override val entries: Set = container.entries override fun get(url: Url): Resource? { - val originalResource = container.get(url) + val originalResource = container[url] ?: return null return transformers - .fold(originalResource) { acc: Resource, transformer: (Url, Resource) -> Resource -> + .fold(originalResource) { acc: Resource, transformer: EntryTransformer -> transformer(url, acc) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt index 3b9209bbcc..2f7dcb6547 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt @@ -37,7 +37,13 @@ public abstract class TransformingResource( ): TransformingResource = object : TransformingResource(resource) { override suspend fun transform(data: Try): Try = - data.flatMap { transform(it) } + data.flatMap { + try { + transform(it) + } catch (e: OutOfMemoryError) { + Try.failure(ReadError.OutOfMemory(e)) + } + } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt index 7398d1b371..169012e2fd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt @@ -62,9 +62,9 @@ public class HtmlResourceContentExtractor : ResourceContentExtractor { .readAsString() .tryRecover { when (it) { - is DecoderError.DataAccess -> + is DecoderError.Read -> return@withContext Try.failure(it.cause) - is DecoderError.DecodingError -> + is DecoderError.Decoding -> Try.success("") } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index c7289459fc..99110b5d2e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.readFully import org.readium.r2.shared.extensions.tryOrLog +import org.readium.r2.shared.extensions.unwrapInstance import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.RelativeUrl @@ -20,7 +21,6 @@ import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException -import org.readium.r2.shared.util.data.unwrapReadException import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream import org.readium.r2.shared.util.mediatype.MediaType @@ -63,7 +63,7 @@ internal class ChannelZipContainer( blob = this ).tryRecover { error -> when (error) { - is MediaTypeSnifferError.DataAccess -> + is MediaTypeSnifferError.Read -> Try.failure(error.cause) MediaTypeSnifferError.NotRecognized -> Try.success(MediaType.BINARY) @@ -98,7 +98,7 @@ internal class ChannelZipContainer( } Try.success(bytes) } catch (exception: Exception) { - when (val e = exception.unwrapReadException()) { + when (val e = exception.unwrapInstance(ReadException::class.java)) { is ReadException -> Try.failure(e.error) else -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 077aaa5f6b..902f42993e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -10,6 +10,7 @@ import java.io.File import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.readium.r2.shared.extensions.unwrapInstance import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.archive.ArchiveFactory @@ -18,7 +19,6 @@ import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException -import org.readium.r2.shared.util.data.unwrapReadException import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeRetriever @@ -52,9 +52,9 @@ public class StreamingZipArchiveProvider( openBlob(blob, ::ReadException, null) Try.success(MediaType.ZIP) } catch (exception: Exception) { - when (val e = exception.unwrapReadException()) { + when (val e = exception.unwrapInstance(ReadException::class.java)) { is ReadException -> - Try.failure(MediaTypeSnifferError.DataAccess(e.error)) + Try.failure(MediaTypeSnifferError.Read(e.error)) else -> Try.failure(MediaTypeSnifferError.NotRecognized) } @@ -62,7 +62,7 @@ public class StreamingZipArchiveProvider( } override suspend fun create( - resource: Blob, + blob: Blob, password: String? ): Try, ArchiveFactory.Error> { if (password != null) { @@ -71,17 +71,17 @@ public class StreamingZipArchiveProvider( return try { val container = openBlob( - resource, + blob, ::ReadException, - resource.source + blob.source ) Try.success(container) } catch (exception: Exception) { - when (val e = exception.unwrapReadException()) { + when (val e = exception.unwrapInstance(ReadException::class.java)) { is ReadException -> - Try.failure(ArchiveFactory.Error.ResourceError(e.error)) + Try.failure(ArchiveFactory.Error.ReadError(e.error)) else -> - Try.failure(ArchiveFactory.Error.ResourceError(ReadError.Decoding(e))) + Try.failure(ArchiveFactory.Error.ReadError(ReadError.Decoding(e))) } } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index fc8f44e4bd..293ce5d3bc 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -12,9 +12,9 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.data.CompositeContainer import org.readium.r2.shared.util.data.DecoderError import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.RoutingContainer import org.readium.r2.shared.util.data.readAsRwpm import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpClient @@ -80,8 +80,8 @@ internal class ParserAssetFactory( val manifest = asset.resource.readAsRwpm() .mapFailure { when (it) { - is DecoderError.DecodingError -> ReadError.Decoding(it.cause) - is DecoderError.DataAccess -> it.cause + is DecoderError.Decoding -> ReadError.Decoding(it.cause) + is DecoderError.Read -> it.cause } } .getOrElse { return Try.failure(Error.ReadError(it)) } @@ -111,12 +111,12 @@ internal class ParserAssetFactory( .toSet() val container = - RoutingContainer( - local = SingleResourceContainer( + CompositeContainer( + SingleResourceContainer( url = Url("manifest.json")!!, asset.resource ), - remote = HttpContainer(httpClient, baseUrl, resources) + HttpContainer(httpClient, baseUrl, resources) ) return Try.success( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 4f53bff966..2be5b1e595 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -200,9 +200,9 @@ public class EpubParser( return decode() .mapFailure { when (it) { - is DecoderError.DataAccess -> + is DecoderError.Read -> PublicationParser.Error.ReadError(it.cause) - is DecoderError.DecodingError -> + is DecoderError.Decoding -> PublicationParser.Error.ReadError( ReadError.Decoding( MessageError( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt index 8237393571..72f773540e 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt @@ -14,12 +14,12 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.PositionsService -import org.readium.r2.shared.util.e import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory import org.readium.r2.shared.util.pdf.cachedIn +import org.readium.r2.shared.util.toDebugDescription import timber.log.Timber /** @@ -104,7 +104,7 @@ internal class LcpdfPositionsService( .cachedIn(context.services) .open(resource, password = null) .getOrElse { - Timber.e(it) + Timber.e(it.toDebugDescription()) null } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index af6c91d3bc..1665d149a2 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -47,13 +47,13 @@ public class ReadiumWebPubParser( ?.readAsRwpm() ?.getOrElse { when (it) { - is DecoderError.DataAccess -> + is DecoderError.Read -> return Try.failure( PublicationParser.Error.ReadError( ReadError.Decoding(it.cause) ) ) - is DecoderError.DecodingError -> + is DecoderError.Decoding -> return Try.failure( PublicationParser.Error.ReadError( ReadError.Decoding( diff --git a/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt b/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt index c28426cecb..81a2a5c70f 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt @@ -16,8 +16,8 @@ import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar +import org.readium.r2.shared.util.toDebugDescription import org.readium.r2.testapp.domain.ImportUserError -import org.readium.r2.testapp.utils.extensions.readium.e import org.readium.r2.testapp.utils.getUserMessage import timber.log.Timber @@ -63,7 +63,7 @@ class MainActivity : AppCompatActivity() { getString(R.string.import_publication_success) is MainViewModel.Event.ImportPublicationError -> { - Timber.e(event.error) + Timber.e(event.error.toDebugDescription()) ImportUserError(event.error).getUserMessage(this) } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 765986df07..d0527b42eb 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -42,9 +42,8 @@ class Readium(context: Context) { mediaTypeRetriever = mediaTypeRetriever ) - private val archiveProviders = listOf( + private val archiveProvider = StreamingZipArchiveProvider(mediaTypeRetriever) - ) private val resourceFactory = CompositeResourceFactory( FileResourceFactory(mediaTypeRetriever), @@ -54,7 +53,7 @@ class Readium(context: Context) { val assetRetriever = AssetRetriever( resourceFactory, - archiveProviders + archiveProvider ) val downloadManager = AndroidDownloadManager( diff --git a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt index d6e4d74ab8..63c2715028 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt @@ -23,6 +23,7 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.toDebugDescription import org.readium.r2.testapp.Application import org.readium.r2.testapp.R import org.readium.r2.testapp.data.model.Book @@ -30,7 +31,6 @@ import org.readium.r2.testapp.databinding.FragmentBookshelfBinding import org.readium.r2.testapp.opds.GridAutoFitLayoutManager import org.readium.r2.testapp.reader.OpeningUserError import org.readium.r2.testapp.reader.ReaderActivityContract -import org.readium.r2.testapp.utils.extensions.readium.e import org.readium.r2.testapp.utils.getUserMessage import org.readium.r2.testapp.utils.viewLifecycle import timber.log.Timber @@ -159,7 +159,7 @@ class BookshelfFragment : Fragment() { val message = when (event) { is BookshelfViewModel.Event.OpenPublicationError -> { - Timber.e(event.error) + Timber.e(event.error.toDebugDescription()) OpeningUserError(event.error).getUserMessage(requireContext()) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index 0a0e320d94..e1daaaddaa 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -40,7 +40,7 @@ sealed class PublicationError( operator fun invoke(error: AssetRetriever.Error): PublicationError = when (error) { - is AssetRetriever.Error.AccessError -> + is AssetRetriever.Error.ReadError -> PublicationError(error) is AssetRetriever.Error.ArchiveFormatNotSupported -> UnsupportedArchiveFormat(error) diff --git a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt index 685abcb617..060e79461c 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt @@ -23,11 +23,11 @@ import org.joda.time.format.DateTimeFormat import org.readium.r2.lcp.MaterialRenewListener import org.readium.r2.lcp.lcpLicense import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.toDebugDescription import org.readium.r2.testapp.R import org.readium.r2.testapp.databinding.FragmentDrmManagementBinding import org.readium.r2.testapp.reader.ReaderViewModel import org.readium.r2.testapp.utils.UserError -import org.readium.r2.testapp.utils.extensions.readium.w import org.readium.r2.testapp.utils.getUserMessage import org.readium.r2.testapp.utils.viewLifecycle import timber.log.Timber @@ -143,5 +143,5 @@ private fun Error.toastUserMessage(view: View) { Snackbar.make(view, getUserMessage(view.context), Snackbar.LENGTH_LONG).show() } - Timber.w(this) + Timber.w(toDebugDescription()) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt index 9d8550ee55..46ade8f460 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt @@ -33,6 +33,7 @@ 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.data.ReadError +import org.readium.r2.shared.util.toDebugDescription import org.readium.r2.testapp.Application import org.readium.r2.testapp.R import org.readium.r2.testapp.data.BookRepository @@ -45,7 +46,6 @@ import org.readium.r2.testapp.search.SearchUserError import org.readium.r2.testapp.utils.EventChannel import org.readium.r2.testapp.utils.UserError import org.readium.r2.testapp.utils.createViewModelFactory -import org.readium.r2.testapp.utils.extensions.readium.e import timber.log.Timber @OptIn( @@ -222,7 +222,7 @@ class ReaderViewModel( _searchLocators.value = emptyList() searchIterator = publication.search(query) .onFailure { - Timber.e(it) + Timber.e(it.toDebugDescription()) activityChannel.send(ActivityCommand.ToastError(SearchUserError(it))) } .getOrNull() @@ -269,7 +269,7 @@ class ReaderViewModel( // Navigator.Listener override fun onResourceLoadFailed(href: Url, error: ReadError) { - Timber.e(error) + Timber.e(error.toDebugDescription()) activityChannel.send( ActivityCommand.ToastError( ReaderUserError(ReadUserError(error)) diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt index e9a77e1a15..b8ae8c7364 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt @@ -50,6 +50,7 @@ import org.readium.r2.navigator.util.DirectionalNavigationAdapter import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.util.Language +import org.readium.r2.shared.util.toDebugDescription import org.readium.r2.testapp.R import org.readium.r2.testapp.data.model.Highlight import org.readium.r2.testapp.databinding.FragmentReaderBinding @@ -59,7 +60,6 @@ import org.readium.r2.testapp.reader.tts.TtsUserError import org.readium.r2.testapp.reader.tts.TtsViewModel import org.readium.r2.testapp.utils.* import org.readium.r2.testapp.utils.extensions.confirmDialog -import org.readium.r2.testapp.utils.extensions.readium.e import org.readium.r2.testapp.utils.extensions.throttleLatest import timber.log.Timber @@ -219,7 +219,7 @@ abstract class VisualReaderFragment : BaseReaderFragment() { .onEach { event -> when (event) { is TtsViewModel.Event.OnError -> { - Timber.e(event.error) + Timber.e(event.error.toDebugDescription()) showError(TtsUserError(event.error)) } is TtsViewModel.Event.OnMissingVoiceData -> diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/ErrorExt.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/ErrorExt.kt deleted file mode 100644 index 2460082a85..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/extensions/readium/ErrorExt.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.testapp.utils.extensions.readium - -import android.content.Context -import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.ThrowableError -import org.readium.r2.testapp.utils.UserError -import org.readium.r2.testapp.utils.getUserMessage -import timber.log.Timber - -/** - * Convenience function to get the description of an error with its cause. - */ -fun Error.toDebugDescription(context: Context): String = - if (this is ThrowableError<*>) { - throwable.toDebugDescription(context) - } else { - var desc = "${javaClass.nameWithEnclosingClasses()}: $message" - cause?.let { cause -> - desc += "\n\n${cause.toDebugDescription(context)}" - } - desc - } - -fun Throwable.toDebugDescription(context: Context): String { - var desc = "${javaClass.nameWithEnclosingClasses()}: " - - desc += (this as? UserError)?.getUserMessage(context) - ?: localizedMessage ?: message ?: "" - desc += "\n" + stackTrace.take(2).joinToString("\n").prependIndent(" ") - cause?.let { cause -> - desc += "\n\n${cause.toDebugDescription(context)}" - } - return desc -} - -private fun Class<*>.nameWithEnclosingClasses(): String { - var name = simpleName - enclosingClass?.let { - name = "${it.nameWithEnclosingClasses()}.$name" - } - return name -} - -// FIXME: to improve -fun Timber.Forest.e(error: Error, message: String? = null) { - e(Exception(error.message), message) -} - -fun Timber.Forest.w(error: Error, message: String? = null) { - w(Exception(error.message), message) -} - -/** - * Finds the first cause instance of the given type. - */ -inline fun Error.asInstance(): T? = - asInstance(T::class.java) - -/** - * Finds the first cause instance of the given type. - */ -fun Error.asInstance(klass: Class): R? = - @Suppress("UNCHECKED_CAST") - when { - klass.isInstance(this) -> this as R - else -> cause?.asInstance(klass) - } From 69d9dd98261299acc316d70ca9207831090345fc Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 20 Nov 2023 13:05:48 +0100 Subject: [PATCH 16/86] Fix uses of Try.assertSuccess --- .../adapter/exoplayer/audio/ExoPlayerDataSource.kt | 6 ++++-- .../main/java/org/readium/r2/lcp/LcpContentProtection.kt | 2 +- .../lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt | 5 +++-- .../license/container/LcplResourceLicenseContainer.kt | 9 ++++----- .../org/readium/navigator/media2/ExoPlayerDataSource.kt | 6 ++++-- .../main/java/org/readium/r2/navigator/R2BasicWebView.kt | 3 +-- .../readium/r2/navigator/audio/PublicationDataSource.kt | 6 ++++-- .../src/main/java/org/readium/r2/shared/util/Try.kt | 2 +- .../readium/r2/shared/util/resource/StringResource.kt | 3 +-- .../org/readium/r2/testapp/search/SearchPagingSource.kt | 7 +++++-- 10 files changed, 28 insertions(+), 21 deletions(-) diff --git a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt index a6a779950e..401395caf7 100644 --- a/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt +++ b/readium/adapters/exoplayer/audio/src/main/java/org/readium/adapter/exoplayer/audio/ExoPlayerDataSource.kt @@ -16,7 +16,8 @@ import androidx.media3.datasource.TransferListener import java.io.IOException import kotlinx.coroutines.runBlocking import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.buffered import org.readium.r2.shared.util.toUrl @@ -116,7 +117,8 @@ internal class ExoPlayerDataSource internal constructor( val data = runBlocking { openedResource.resource .read(range = openedResource.position until (openedResource.position + length)) - .assertSuccess() + .mapFailure { ReadException(it) } + .getOrThrow() } if (data.isEmpty()) { diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index cae810f317..5fc15009dc 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -37,7 +37,7 @@ internal class LcpContentProtection( override suspend fun supports( asset: Asset - ): Try = + ): Try = Try.success(lcpService.isLcpProtected(asset)) override suspend fun open( diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index b215e7c374..71ff089045 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -17,7 +17,6 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse @@ -264,7 +263,9 @@ internal class LcpDecryptor( val rangeLength = if (lastBlockRead) { // use decrypted length to ensure range.last doesn't exceed decrypted length - 1 - range.last.coerceAtMost(length().assertSuccess() - 1) - range.first + 1 + val decryptedLength = length() + .getOrElse { return Try.failure(it) } + range.last.coerceAtMost(decryptedLength - 1) - range.first + 1 } else { // the last block won't be read, so there's no need to compute length range.last - range.first + 1 diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt index c1d3628f14..8d5fa01c50 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LcplResourceLicenseContainer.kt @@ -12,7 +12,7 @@ package org.readium.r2.lcp.license.container import kotlinx.coroutines.runBlocking import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException -import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.Resource /** @@ -22,9 +22,8 @@ import org.readium.r2.shared.util.resource.Resource internal class LcplResourceLicenseContainer(private val resource: Resource) : LicenseContainer { override fun read(): ByteArray = - try { - runBlocking { resource.read().assertSuccess() } - } catch (e: Exception) { - throw LcpException(LcpError.Container.OpenFailed) + runBlocking { + resource.read() + .getOrElse { throw LcpException(LcpError.Container.OpenFailed) } } } diff --git a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt index 29ea63f923..12807146f0 100644 --- a/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt +++ b/readium/navigator-media2/src/main/java/org/readium/navigator/media2/ExoPlayerDataSource.kt @@ -19,7 +19,8 @@ import com.google.android.exoplayer2.upstream.TransferListener import java.io.IOException import kotlinx.coroutines.runBlocking import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.buffered import org.readium.r2.shared.util.toUrl @@ -118,7 +119,8 @@ public class ExoPlayerDataSource internal constructor(private val publication: P val data = runBlocking { openedResource.resource .read(range = openedResource.position until (openedResource.position + length)) - .assertSuccess() + .mapFailure { ReadException(it) } + .getOrThrow() } if (data.isEmpty()) { diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt index bc3569f9e0..eb72c2cc93 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt @@ -47,7 +47,6 @@ import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.data.readAsString import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.toUrl @@ -352,7 +351,7 @@ internal open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebV ?.use { res -> res.readAsString() .map { Jsoup.parse(it) } - .assertSuccess() + .getOrNull() } ?.select("#$id") ?.first()?.html() diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt index 13b2a7c0fb..68f0d59b3a 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt @@ -19,7 +19,8 @@ import com.google.android.exoplayer2.upstream.TransferListener import java.io.IOException import kotlinx.coroutines.runBlocking import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.buffered import org.readium.r2.shared.util.toUrl @@ -118,7 +119,8 @@ internal class PublicationDataSource(private val publication: Publication) : Bas val data = runBlocking { openedResource.resource .read(range = openedResource.position until (openedResource.position + length)) - .assertSuccess() + .mapFailure { ReadException(it) } + .getOrThrow() } if (data.isEmpty()) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt index afb4743859..a7be7aaf93 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt @@ -156,6 +156,6 @@ public fun Try.assertSuccess(): S = is Try.Failure -> throw IllegalStateException( "Try was excepted to contain a success.", - value as? Throwable + value as? Throwable ?: (value as? Error)?.let { ErrorException(it) } ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt index 2a5af9f953..ee7558295d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt @@ -9,7 +9,6 @@ package org.readium.r2.shared.util.resource import kotlinx.coroutines.runBlocking import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.InMemoryBlob import org.readium.r2.shared.util.data.ReadError @@ -43,5 +42,5 @@ public class StringResource( Try.success(properties) override fun toString(): String = - "${javaClass.simpleName}(${runBlocking { read().assertSuccess().decodeToString() } }})" + "${javaClass.simpleName}(${runBlocking { read().map { it.decodeToString() } } }})" } diff --git a/test-app/src/main/java/org/readium/r2/testapp/search/SearchPagingSource.kt b/test-app/src/main/java/org/readium/r2/testapp/search/SearchPagingSource.kt index 06227a355a..91b82d1f10 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/search/SearchPagingSource.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/search/SearchPagingSource.kt @@ -12,7 +12,8 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.LocatorCollection import org.readium.r2.shared.publication.services.search.SearchTry -import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.getOrThrow @OptIn(ExperimentalReadiumApi::class) class SearchPagingSource( @@ -31,7 +32,9 @@ class SearchPagingSource( listener ?: return LoadResult.Page(data = emptyList(), prevKey = null, nextKey = null) return try { - val page = listener.next().assertSuccess() + val page = listener.next() + .mapFailure { ErrorException(it) } + .getOrThrow() LoadResult.Page( data = page?.locators ?: emptyList(), prevKey = null, From e720ebc6a703fbd4a546e1627ecebd8ff4746897 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 20 Nov 2023 13:31:49 +0100 Subject: [PATCH 17/86] Fix PsPDFKit errors --- .../adapter/pspdfkit/document/PsPdfKitDocument.kt | 13 +++++-------- .../readium/r2/shared/util/data/BlobInputStream.kt | 9 --------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt index 761039111f..844942415c 100644 --- a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt @@ -36,21 +36,18 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory = PsPdfKitDocument::class override suspend fun open(resource: Resource, password: String?): ResourceTry = - open(context, DocumentSource(ResourceDataProvider(resource), password)) - - // FIXME : error handling is too rough - private suspend fun open(context: Context, documentSource: DocumentSource): ResourceTry = withContext(Dispatchers.IO) { + val dataProvider = ResourceDataProvider(resource) + val documentSource = DocumentSource(dataProvider) try { - Try.success( - PsPdfKitDocument(PdfDocumentLoader.openDocument(context, documentSource)) - ) + val innerDocument = PdfDocumentLoader.openDocument(context, documentSource) + Try.success(PsPdfKitDocument(innerDocument)) } catch (e: InvalidPasswordException) { Try.failure(ReadError.Decoding(ThrowableError(e))) } catch (e: InvalidSignatureException) { Try.failure(ReadError.Decoding(ThrowableError(e))) } catch (e: IOException) { - Try.failure(ReadError.Other(e)) + Try.failure(dataProvider.error!!) } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt index 44ff4d7b51..1d69c8becf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt @@ -45,14 +45,6 @@ public class BlobInputStream( */ private var mark: Long = range?.start ?: 0 - private var error: ReadError? = null - - internal fun consumeError(): ReadError? { - val errorNow = error - error = null - return errorNow - } - override fun available(): Int { checkNotClosed() return (end - position).toInt() @@ -146,7 +138,6 @@ public class BlobInputStream( value } is Try.Failure -> { - error = value throw wrapError(value) } } From 7053cd360456f0b4aa84b64c649f5c527f4b75ef Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 20 Nov 2023 14:54:00 +0100 Subject: [PATCH 18/86] Small fixes --- .../ContentProtectionSchemeRetriever.kt | 5 ++- .../r2/shared/util/data/CompositeContainer.kt | 35 ------------------- .../readium/r2/shared/util/data/Container.kt | 26 ++++++++++++++ .../readium/r2/testapp/domain/Bookshelf.kt | 10 +++++- .../r2/testapp/domain/PublicationError.kt | 2 +- 5 files changed, 38 insertions(+), 40 deletions(-) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/data/CompositeContainer.kt diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt index 5df2c726db..cdc26688f5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt @@ -10,7 +10,6 @@ import kotlin.String import kotlin.let import kotlin.takeIf import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse /** @@ -33,14 +32,14 @@ public class ContentProtectionSchemeRetriever( public object NoContentProtectionFound : Error("No content protection recognized the given asset.", null) - public class AccessError(override val cause: ReadError) : + public class ReadError(override val cause: org.readium.r2.shared.util.data.ReadError) : Error("An error occurred while trying to read asset.", cause) } public suspend fun retrieve(asset: org.readium.r2.shared.util.asset.Asset): Try { for (protection in contentProtections) { protection.supports(asset) - .getOrElse { return Try.failure(Error.AccessError(it)) } + .getOrElse { return Try.failure(Error.ReadError(it)) } .takeIf { it } ?.let { return Try.success(protection.scheme) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/CompositeContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/CompositeContainer.kt deleted file mode 100644 index 6166e41399..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/CompositeContainer.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.data - -import org.readium.r2.shared.util.Url - -/** - * Routes requests to child containers, depending on a provided predicate. - * - * This can be used for example to serve a publication containing both local and remote resources, - * and more generally to concatenate different content sources. - * - * The [containers] will be tested in the given order. - */ -public class CompositeContainer( - private val containers: List> -) : Container { - - public constructor(vararg containers: Container) : - this(containers.toList()) - - override val entries: Set = - containers.fold(emptySet()) { acc, container -> acc + container.entries } - - override fun get(url: Url): E? = - containers.firstNotNullOfOrNull { it[url] } - - override suspend fun close() { - containers.forEach { it.close() } - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index a200a4cf04..4b0dc356cf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -45,3 +45,29 @@ public class EmptyContainer : override suspend fun close() {} } + +/** + * Routes requests to child containers, depending on a provided predicate. + * + * This can be used for example to serve a publication containing both local and remote resources, + * and more generally to concatenate different content sources. + * + * The [containers] will be tested in the given order. + */ +public class CompositeContainer( + private val containers: List> +) : Container { + + public constructor(vararg containers: Container) : + this(containers.toList()) + + override val entries: Set = + containers.fold(emptySet()) { acc, container -> acc + container.entries } + + override fun get(url: Url): E? = + containers.firstNotNullOfOrNull { it[url] } + + override suspend fun close() { + containers.forEach { it.close() } + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index 772a94afce..64a2c3f35c 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -23,6 +23,7 @@ import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.toUrl +import org.readium.r2.shared.util.tryRecover import org.readium.r2.streamer.PublicationFactory import org.readium.r2.testapp.data.BookRepository import org.readium.r2.testapp.data.model.Book @@ -135,7 +136,14 @@ class Bookshelf( val drmScheme = protectionRetriever.retrieve(asset) - .getOrElse { + .tryRecover { + when (it) { + ContentProtectionSchemeRetriever.Error.NoContentProtectionFound -> + Try.success(null) + is ContentProtectionSchemeRetriever.Error.ReadError -> + Try.failure(it) + } + }.getOrElse { return Try.failure( ImportError.PublicationError(PublicationError(it)) ) diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index e1daaaddaa..e5d0d391fd 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -50,7 +50,7 @@ sealed class PublicationError( operator fun invoke(error: ContentProtectionSchemeRetriever.Error): PublicationError = when (error) { - is ContentProtectionSchemeRetriever.Error.AccessError -> + is ContentProtectionSchemeRetriever.Error.ReadError -> PublicationError(error) ContentProtectionSchemeRetriever.Error.NoContentProtectionFound -> UnsupportedContentProtection(error) From e25514d975ef47db330febab156a4d78d0525039 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 20 Nov 2023 15:12:42 +0100 Subject: [PATCH 19/86] Small fixes --- .../org/readium/r2/shared/util/http/HttpContainer.kt | 2 ++ .../shared/util/mediatype/DefaultMediaTypeSniffer.kt | 10 +++++----- .../r2/shared/util/mediatype/MediaTypeRetriever.kt | 9 +++------ .../r2/shared/util/mediatype/MediaTypeSniffer.kt | 12 ++++++------ .../org/readium/r2/testapp/data/BookRepository.kt | 7 ++++--- .../java/org/readium/r2/testapp/domain/Bookshelf.kt | 2 +- .../readium/r2/testapp/domain/PublicationError.kt | 6 +++--- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt index fce7648d1c..da21974c47 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt @@ -27,6 +27,8 @@ public class HttpContainer( ) : Container { override fun get(url: Url): Resource? { + // We don't check that url matches any entry because that might save us from edge cases. + val absoluteUrl = (baseUrl?.resolve(url) ?: url) as? AbsoluteUrl return if (absoluteUrl == null || !absoluteUrl.isHttp) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt index e55834f356..86faf59917 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt @@ -19,15 +19,15 @@ public class DefaultMediaTypeSniffer : MediaTypeSniffer { private val sniffer: MediaTypeSniffer = CompositeMediaTypeSniffer( listOf( - XhtmlMediaTypeSniffer(), - HtmlMediaTypeSniffer(), + XhtmlMediaTypeSniffer, + HtmlMediaTypeSniffer, OpdsMediaTypeSniffer, LcpLicenseMediaTypeSniffer, BitmapMediaTypeSniffer, - WebPubManifestMediaTypeSniffer(), - WebPubMediaTypeSniffer(), + WebPubManifestMediaTypeSniffer, + WebPubMediaTypeSniffer, W3cWpubMediaTypeSniffer, - EpubMediaTypeSniffer(), + EpubMediaTypeSniffer, LpfMediaTypeSniffer, ArchiveMediaTypeSniffer, PdfMediaTypeSniffer, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt index 367ab46e25..9ba9ba0e49 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt @@ -28,9 +28,6 @@ public class MediaTypeRetriever( private val mediaTypeSniffer: MediaTypeSniffer = DefaultMediaTypeSniffer() ) : MediaTypeSniffer { - private val systemMediaTypeSniffer: MediaTypeSniffer = - SystemMediaTypeSniffer() - /** * Retrieves a canonical [MediaType] for the provided media type and file extension [hints]. */ @@ -43,7 +40,7 @@ public class MediaTypeRetriever( // Note: This is done after the default sniffers, because otherwise it will detect // JSON, XML or ZIP formats before we have a chance of sniffing their content (for example, // for RWPM). - systemMediaTypeSniffer.sniffHints(hints) + SystemMediaTypeSniffer.sniffHints(hints) .getOrNull() ?.let { return it } @@ -133,12 +130,12 @@ public class MediaTypeRetriever( // Note: This is done after the default sniffers, because otherwise it will detect // JSON, XML or ZIP formats before we have a chance of sniffing their content (for example, // for RWPM). - systemMediaTypeSniffer.sniffHints(hints) + SystemMediaTypeSniffer.sniffHints(hints) .getOrNull() ?.let { return Try.success(it) } if (blob != null) { - systemMediaTypeSniffer.sniffBlob(blob) + SystemMediaTypeSniffer.sniffBlob(blob) .onSuccess { return Try.success(it) } .onFailure { error -> when (error) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index b2cfcc4b34..cee864a069 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -148,7 +148,7 @@ internal class CompositeMediaTypeSniffer( * * Must precede the HTML sniffer. */ -public class XhtmlMediaTypeSniffer : MediaTypeSniffer { +public object XhtmlMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("xht", "xhtml") || @@ -184,7 +184,7 @@ public class XhtmlMediaTypeSniffer : MediaTypeSniffer { } /** Sniffs an HTML document. */ -public class HtmlMediaTypeSniffer : MediaTypeSniffer { +public object HtmlMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("htm", "html") || @@ -421,7 +421,7 @@ public object BitmapMediaTypeSniffer : MediaTypeSniffer { } /** Sniffs a Readium Web Manifest. */ -public class WebPubManifestMediaTypeSniffer : MediaTypeSniffer { +public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try { if (hints.hasMediaType("application/audiobook+json")) { return Try.success(MediaType.READIUM_AUDIOBOOK_MANIFEST) @@ -468,7 +468,7 @@ public class WebPubManifestMediaTypeSniffer : MediaTypeSniffer { } /** Sniffs a Readium Web Publication, protected or not by LCP. */ -public class WebPubMediaTypeSniffer : MediaTypeSniffer { +public object WebPubMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("audiobook") || @@ -571,7 +571,7 @@ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { * * Reference: https://www.w3.org/publishing/epub3/epub-ocf.html#sec-zip-container-mime */ -public class EpubMediaTypeSniffer : MediaTypeSniffer { +public object EpubMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try { if ( hints.hasFileExtension("epub") || @@ -795,7 +795,7 @@ public object JsonMediaTypeSniffer : MediaTypeSniffer { * Sniffs the system-wide registered media types using [MimeTypeMap] and * [URLConnection.guessContentTypeFromStream]. */ -public class SystemMediaTypeSniffer : MediaTypeSniffer { +public object SystemMediaTypeSniffer : MediaTypeSniffer { private val mimetypes = tryOrNull { MimeTypeMap.getSingleton() } diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt index 631e4810ff..218ba91809 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt @@ -15,6 +15,7 @@ import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.indexOfFirstWithHref import org.readium.r2.shared.publication.protection.ContentProtection +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.testapp.data.db.BooksDao import org.readium.r2.testapp.data.model.Book @@ -79,7 +80,7 @@ class BookRepository( } suspend fun insertBook( - href: String, + url: Url, mediaType: MediaType, containerType: MediaType?, drm: ContentProtection.Scheme?, @@ -88,9 +89,9 @@ class BookRepository( ): Long { val book = Book( creation = DateTime().toDate().time, - title = publication.metadata.title, + title = publication.metadata.title ?: url.filename, author = publication.metadata.authorName, - href = href, + href = url.toString(), identifier = publication.metadata.identifier ?: "", mediaType = mediaType, containerType = containerType, diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index 64a2c3f35c..41ec250a08 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -165,7 +165,7 @@ class Bookshelf( } val id = bookRepository.insertBook( - url.toString(), + url, asset.mediaType, (asset as? Asset.Container)?.containerType, drmScheme, diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index e5d0d391fd..4170c32814 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -41,7 +41,7 @@ sealed class PublicationError( operator fun invoke(error: AssetRetriever.Error): PublicationError = when (error) { is AssetRetriever.Error.ReadError -> - PublicationError(error) + ReadError(error.cause) is AssetRetriever.Error.ArchiveFormatNotSupported -> UnsupportedArchiveFormat(error) is AssetRetriever.Error.SchemeNotSupported -> @@ -51,7 +51,7 @@ sealed class PublicationError( operator fun invoke(error: ContentProtectionSchemeRetriever.Error): PublicationError = when (error) { is ContentProtectionSchemeRetriever.Error.ReadError -> - PublicationError(error) + ReadError(error.cause) ContentProtectionSchemeRetriever.Error.NoContentProtectionFound -> UnsupportedContentProtection(error) } @@ -59,7 +59,7 @@ sealed class PublicationError( operator fun invoke(error: PublicationFactory.Error): PublicationError = when (error) { is PublicationFactory.Error.ReadError -> - PublicationError(error) + ReadError(error.cause) is PublicationFactory.Error.UnsupportedAsset -> UnsupportedPublication(error) is PublicationFactory.Error.UnsupportedContentProtection -> From d89a6dd515896893f282740c77f7df475fa1943b Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 20 Nov 2023 17:40:02 +0100 Subject: [PATCH 20/86] Fix sniffing and AssetRetriever --- .../r2/shared/util/asset/AssetRetriever.kt | 65 ++++++++++++------- .../util/mediatype/DefaultMediaTypeSniffer.kt | 14 ++-- .../shared/util/mediatype/MediaTypeSniffer.kt | 53 +++++++++++---- .../readium/r2/testapp/domain/Bookshelf.kt | 2 + .../r2/testapp/domain/PublicationRetriever.kt | 3 +- 5 files changed, 93 insertions(+), 44 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 82fa4189f4..d8d8d7ab54 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -14,12 +14,15 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.archive.ArchiveProvider import org.readium.r2.shared.util.archive.FileZipArchiveProvider +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.getOrElse 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.toUrl +import org.readium.r2.shared.util.tryRecover /** * Retrieves an [Asset] instance providing reading access to the resource(s) of an asset stored at a @@ -27,7 +30,8 @@ import org.readium.r2.shared.util.toUrl */ public class AssetRetriever( private val resourceFactory: ResourceFactory = FileResourceFactory(), - private val archiveProvider: ArchiveProvider = FileZipArchiveProvider() + private val archiveProvider: ArchiveProvider = FileZipArchiveProvider(), + private val mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever() ) { public sealed class Error( @@ -72,13 +76,6 @@ public class AssetRetriever( val resource = retrieveResource(url, containerType) .getOrElse { return Try.failure(it) } - return retrieveArchiveAsset(resource, mediaType, containerType) - } - private suspend fun retrieveArchiveAsset( - resource: Resource, - mediaType: MediaType, - containerType: MediaType - ): Try { archiveProvider.sniffHints(MediaTypeHints(mediaType = containerType)) .onFailure { return Try.failure( @@ -88,6 +85,13 @@ public class AssetRetriever( ) } + return retrieveArchiveAsset(resource, MediaTypeHints(mediaType = mediaType), containerType) + } + private suspend fun retrieveArchiveAsset( + resource: Resource, + mediaTypeHints: MediaTypeHints, + containerType: MediaType + ): Try { val container = archiveProvider.create(resource) .mapFailure { error -> when (error) { @@ -99,6 +103,17 @@ public class AssetRetriever( } .getOrElse { return Try.failure(it) } + val mediaType = mediaTypeRetriever + .retrieve(mediaTypeHints, container) + .getOrElse { error -> + when (error) { + MediaTypeSnifferError.NotRecognized -> + MediaType.BINARY + is MediaTypeSnifferError.Read -> + return Try.failure(Error.ReadError(error.cause)) + } + } + val asset = Asset.Container( mediaType = mediaType, containerType = containerType, @@ -156,22 +171,24 @@ public class AssetRetriever( ) } - val mediaType = resource.mediaType() - .getOrElse { return Try.failure(Error.ReadError(it)) } - - return archiveProvider.sniffBlob(resource) - .fold( - { containerType -> - retrieveArchiveAsset(url, mediaType = mediaType, containerType = containerType) - }, - { error -> - when (error) { - MediaTypeSnifferError.NotRecognized -> - Try.success(Asset.Resource(mediaType, resource)) - is MediaTypeSnifferError.Read -> - Try.failure(Error.ReadError(error.cause)) - } + val containerType = archiveProvider.sniffBlob(resource) + .tryRecover { error -> + when (error) { + MediaTypeSnifferError.NotRecognized -> + Try.success(null) + is MediaTypeSnifferError.Read -> + return Try.failure(Error.ReadError(error.cause)) } - ) + }.assertSuccess() + + if (containerType == null) { + val mediaType = resource.mediaType() + .getOrElse { return Try.failure(Error.ReadError(it)) } + return Try.success(Asset.Resource(mediaType, resource)) + } + + val hints = MediaTypeHints(fileExtension = url.extension) + + return retrieveArchiveAsset(resource, mediaTypeHints = hints, containerType = containerType) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt index 86faf59917..c353666167 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt @@ -19,18 +19,18 @@ public class DefaultMediaTypeSniffer : MediaTypeSniffer { private val sniffer: MediaTypeSniffer = CompositeMediaTypeSniffer( listOf( - XhtmlMediaTypeSniffer, - HtmlMediaTypeSniffer, - OpdsMediaTypeSniffer, - LcpLicenseMediaTypeSniffer, - BitmapMediaTypeSniffer, - WebPubManifestMediaTypeSniffer, WebPubMediaTypeSniffer, - W3cWpubMediaTypeSniffer, EpubMediaTypeSniffer, LpfMediaTypeSniffer, ArchiveMediaTypeSniffer, PdfMediaTypeSniffer, + BitmapMediaTypeSniffer, + XhtmlMediaTypeSniffer, + HtmlMediaTypeSniffer, + OpdsMediaTypeSniffer, + LcpLicenseMediaTypeSniffer, + W3cWpubMediaTypeSniffer, + WebPubManifestMediaTypeSniffer, JsonMediaTypeSniffer ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index cee864a069..5ce33990e5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -13,6 +13,7 @@ import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject +import org.readium.r2.shared.extensions.asInstance import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Manifest @@ -31,6 +32,7 @@ import org.readium.r2.shared.util.data.readAsJson import org.readium.r2.shared.util.data.readAsRwpm import org.readium.r2.shared.util.data.readAsString import org.readium.r2.shared.util.data.readAsXml +import org.readium.r2.shared.util.getOrDefault import org.readium.r2.shared.util.getOrElse public sealed class MediaTypeSnifferError( @@ -161,6 +163,10 @@ public object XhtmlMediaTypeSniffer : MediaTypeSniffer { } override suspend fun sniffBlob(blob: Blob): Try { + if (!blob.canReadWholeBlob()) { + return Try.failure(MediaTypeSnifferError.NotRecognized) + } + blob.readAsXml() .getOrElse { when (it) { @@ -197,6 +203,10 @@ public object HtmlMediaTypeSniffer : MediaTypeSniffer { } override suspend fun sniffBlob(blob: Blob): Try { + if (!blob.canReadWholeBlob()) { + return Try.failure(MediaTypeSnifferError.NotRecognized) + } + // [contentAsXml] will fail if the HTML is not a proper XML document, hence the doctype check. blob.readAsXml() .getOrElse { @@ -265,6 +275,10 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { } override suspend fun sniffBlob(blob: Blob): Try { + if (!blob.canReadWholeBlob()) { + return Try.failure(MediaTypeSnifferError.NotRecognized) + } + // OPDS 1 blob.readAsXml() .getOrElse { @@ -348,6 +362,10 @@ public object LcpLicenseMediaTypeSniffer : MediaTypeSniffer { } override suspend fun sniffBlob(blob: Blob): Try { + if (!blob.canReadWholeBlob()) { + return Try.failure(MediaTypeSnifferError.NotRecognized) + } + blob.containsJsonKeys("id", "issued", "provider", "encryption") .getOrElse { when (it) { @@ -439,6 +457,10 @@ public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { } public override suspend fun sniffBlob(blob: Blob): Try { + if (!blob.canReadWholeBlob()) { + return Try.failure(MediaTypeSnifferError.NotRecognized) + } + val manifest: Manifest = blob.readAsRwpm() .getOrElse { @@ -544,6 +566,10 @@ public object WebPubMediaTypeSniffer : MediaTypeSniffer { /** Sniffs a W3C Web Publication Manifest. */ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { override suspend fun sniffBlob(blob: Blob): Try { + if (!blob.canReadWholeBlob()) { + return Try.failure(MediaTypeSnifferError.NotRecognized) + } + // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. val string = blob.readAsString() .getOrElse { @@ -585,11 +611,15 @@ public object EpubMediaTypeSniffer : MediaTypeSniffer { override suspend fun sniffContainer(container: Container<*>): Try { val mimetype = container[RelativeUrl("mimetype")!!] - ?.read() + ?.readAsString(charset = Charsets.US_ASCII) ?.getOrElse { error -> - return Try.failure(MediaTypeSnifferError.Read(error)) - } - ?.let { String(it, charset = Charsets.US_ASCII).trim() } + when (error) { + is DecoderError.Decoding -> + null + is DecoderError.Read -> + return Try.failure(MediaTypeSnifferError.Read(error.cause)) + } + }?.trim() if (mimetype == "application/epub+zip") { return Try.success(MediaType.EPUB) } @@ -775,6 +805,10 @@ public object JsonMediaTypeSniffer : MediaTypeSniffer { } override suspend fun sniffBlob(blob: Blob): Try { + if (!blob.canReadWholeBlob()) { + return Try.failure(MediaTypeSnifferError.NotRecognized) + } + blob.readAsJson() .getOrElse { when (it) { @@ -822,7 +856,7 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { ?.let { sniffType(it) } } } catch (e: Exception) { - e.findSystemSnifferException() + e.asInstance(SystemSnifferException::class.java) ?.let { return Try.failure( MediaTypeSnifferError.Read(it.error) @@ -838,12 +872,6 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { private class SystemSnifferException( val error: ReadError ) : IOException() - private fun Throwable.findSystemSnifferException(): SystemSnifferException? = - when { - this is SystemSnifferException -> this - cause != null -> cause!!.findSystemSnifferException() - else -> null - } private fun sniffType(type: String): MediaType? { val extension = mimetypes?.getExtensionFromMimeType(type) @@ -857,3 +885,6 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { mimetypes?.getMimeTypeFromExtension(extension) ?.let { MediaType(it) } } + +private suspend fun Blob.canReadWholeBlob() = + length().getOrDefault(0) < 5 * 1000 * 1000 diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index 41ec250a08..f7e4cbac03 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -134,6 +134,8 @@ class Bookshelf( ) } + Timber.d("asset ${asset.mediaType}") + val drmScheme = protectionRetriever.retrieve(asset) .tryRecover { diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index 2fbd3ff3d6..127a0ad27c 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -132,7 +132,6 @@ class LocalPublicationRetriever( ) return@launch } - retrieveFromStorage(tempFile) } } @@ -179,7 +178,7 @@ class LocalPublicationRetriever( return } - val fileExtension = formatRegistry.fileExtension(sourceAsset.mediaType) ?: "epub" + val fileExtension = formatRegistry.fileExtension(sourceAsset.mediaType) ?: tempFile.extension val fileName = "${UUID.randomUUID()}.$fileExtension" val libraryFile = File(storageDir, fileName) From b683e066669a7fe58f810c8a6bb1708411fd4c7c Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 20 Nov 2023 23:59:42 +0100 Subject: [PATCH 21/86] Fix SingleResourceContainer --- .../r2/shared/util/resource/SingleResourceContainer.kt | 6 +++--- .../main/java/org/readium/r2/streamer/ParserAssetFactory.kt | 2 +- .../main/java/org/readium/r2/testapp/domain/Bookshelf.kt | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt index cd1cdff5ec..df27f9e565 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt @@ -11,7 +11,7 @@ import org.readium.r2.shared.util.data.Container /** A [Container] for a single [Resource]. */ public class SingleResourceContainer( - url: Url, + private val entryUrl: Url, private val resource: Resource ) : Container { @@ -24,10 +24,10 @@ public class SingleResourceContainer( } } - override val entries: Set = setOf(url) + override val entries: Set = setOf(entryUrl) override fun get(url: Url): Resource? { - if (url.removeFragment().removeQuery() != url) { + if (url.removeFragment().removeQuery() != entryUrl) { return null } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index 293ce5d3bc..e3c74eca96 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -113,7 +113,7 @@ internal class ParserAssetFactory( val container = CompositeContainer( SingleResourceContainer( - url = Url("manifest.json")!!, + Url("manifest.json")!!, asset.resource ), HttpContainer(httpClient, baseUrl, resources) diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index f7e4cbac03..41ec250a08 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -134,8 +134,6 @@ class Bookshelf( ) } - Timber.d("asset ${asset.mediaType}") - val drmScheme = protectionRetriever.retrieve(asset) .tryRecover { From c99001ecc3dcbb41fd67bb6d8e6397ef4c1fe8ee Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Tue, 21 Nov 2023 00:52:44 +0100 Subject: [PATCH 22/86] Optimize ZIP sniffing --- .../util/archive/FileZipArchiveProvider.kt | 11 +--- .../util/archive/ZipHintMediaTypeSniffer.kt | 52 +++++++++++++++++++ .../r2/shared/util/asset/AssetRetriever.kt | 9 +++- .../util/zip/StreamingZipArchiveProvider.kt | 12 ++--- 4 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipHintMediaTypeSniffer.kt diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt index 4c04c71e8a..f842496f31 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt @@ -33,15 +33,8 @@ public class FileZipArchiveProvider( private val mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever() ) : ArchiveProvider { - override fun sniffHints(hints: MediaTypeHints): Try { - if (hints.hasMediaType("application/zip") || - hints.hasFileExtension("zip") - ) { - return Try.success(MediaType.ZIP) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } + override fun sniffHints(hints: MediaTypeHints): Try = + ZipHintMediaTypeSniffer.sniffHints(hints) override suspend fun sniffBlob(blob: Blob): Try { val file = blob.source?.toFile() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipHintMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipHintMediaTypeSniffer.kt new file mode 100644 index 0000000000..f60ddaf595 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipHintMediaTypeSniffer.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.archive + +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.HintMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeHints +import org.readium.r2.shared.util.mediatype.MediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError + +internal object ZipHintMediaTypeSniffer : HintMediaTypeSniffer { + + private val generalSniffer: MediaTypeSniffer = + DefaultMediaTypeSniffer() + + private val acceptedMediaTypes: List = + listOf( + MediaType.EPUB, + MediaType.READIUM_WEBPUB, + MediaType.READIUM_AUDIOBOOK, + MediaType.DIVINA, + MediaType.LCP_PROTECTED_PDF, + MediaType.LCP_PROTECTED_AUDIOBOOK, + MediaType.LPF, + MediaType.CBZ, + MediaType.ZAB + ) + + override fun sniffHints(hints: MediaTypeHints): Try { + if (hints.hasMediaType("application/zip") || + hints.hasFileExtension("zip") + ) { + return Try.success(MediaType.ZIP) + } + + val mediaType = generalSniffer.sniffHints(hints) + .getOrElse { return Try.failure(it) } + + if (mediaType in acceptedMediaTypes) { + return Try.success(MediaType.ZIP) + } + + return Try.failure(MediaTypeSnifferError.NotRecognized) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index d8d8d7ab54..3f491afab1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -171,8 +171,13 @@ public class AssetRetriever( ) } - val containerType = archiveProvider.sniffBlob(resource) - .tryRecover { error -> + // FIXME: should use HTTP Content-Type but not the resource content + val containerType = archiveProvider.sniffHints( + MediaTypeHints(fileExtension = url.extension) + ) + .tryRecover { + archiveProvider.sniffBlob(resource) + }.tryRecover { error -> when (error) { MediaTypeSnifferError.NotRecognized -> Try.success(null) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 902f42993e..23c564a8fe 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -15,6 +15,7 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.archive.ArchiveProvider +import org.readium.r2.shared.util.archive.ZipHintMediaTypeSniffer import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError @@ -37,15 +38,8 @@ public class StreamingZipArchiveProvider( private val mediaTypeRetriever: MediaTypeRetriever ) : ArchiveProvider { - override fun sniffHints(hints: MediaTypeHints): Try { - if (hints.hasMediaType("application/zip") || - hints.hasFileExtension("zip") - ) { - return Try.success(MediaType.ZIP) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } + override fun sniffHints(hints: MediaTypeHints): Try = + ZipHintMediaTypeSniffer.sniffHints(hints) override suspend fun sniffBlob(blob: Blob): Try { return try { From 5257109993bf9823b03218168778a61a093329cf Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Tue, 21 Nov 2023 08:05:59 +0100 Subject: [PATCH 23/86] Remove ReadError.Other --- .../r2/shared/util/data/ContentProviderError.kt | 2 +- .../readium/r2/shared/util/data/FileSystemError.kt | 2 +- .../org/readium/r2/shared/util/data/ReadError.kt | 12 +++--------- .../org/readium/r2/shared/util/http/HttpError.kt | 3 ++- .../org/readium/r2/testapp/domain/ReadUserError.kt | 1 - 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt index 3939afc2f0..372b48c3d3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt @@ -12,7 +12,7 @@ import org.readium.r2.shared.util.ThrowableError public sealed class ContentProviderError( override val message: String, override val cause: Error? = null -) : Error { +) : AccessError { public class FileNotFound( cause: Error? diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt index 073fdf27a8..1efb14df9c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt @@ -12,7 +12,7 @@ import org.readium.r2.shared.util.ThrowableError public sealed class FileSystemError( override val message: String, override val cause: Error? = null -) : Error { +) : AccessError { public class NotFound( cause: Error? diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt index 3a8da2f3f9..56203ded84 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt @@ -20,7 +20,7 @@ public sealed class ReadError( override val cause: Error? = null ) : Error { - public class Access(public override val cause: Error) : + public class Access(public override val cause: AccessError) : ReadError("An error occurred while attempting to access data.", cause) public class Decoding(cause: Error? = null) : @@ -41,16 +41,10 @@ public sealed class ReadError( public constructor(message: String) : this(MessageError(message)) } - - /** For any other error, such as HTTP 500. */ - public class Other(cause: Error) : - ReadError("An unclassified error occurred.", cause) { - - public constructor(message: String) : this(MessageError(message)) - public constructor(exception: Exception) : this(ThrowableError(exception)) - } } +public interface AccessError : Error + public class ReadException( public val error: ReadError ) : IOException(error.message, ErrorException(error)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt index 8b4345928c..7325a5d9c6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt @@ -10,6 +10,7 @@ import org.json.JSONObject import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.data.AccessError import org.readium.r2.shared.util.mediatype.MediaType /** @@ -18,7 +19,7 @@ import org.readium.r2.shared.util.mediatype.MediaType public sealed class HttpError( public override val message: String, public override val cause: Error? = null -) : Error { +) : AccessError { public class MalformedResponse(cause: Error?) : HttpError("The received response could not be decoded.", cause) diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt index 995c410ba2..b2badcec06 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt @@ -62,7 +62,6 @@ sealed class ReadUserError( else -> Unexpected(cause) } is ReadError.Decoding -> InvalidPublication(error) - is ReadError.Other -> Unexpected(error) is ReadError.OutOfMemory -> OutOfMemory(error) is ReadError.UnsupportedOperation -> Unexpected(error) } From 7d559857087c41b25ab82c178747c4c21296627a Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Tue, 21 Nov 2023 11:57:56 +0100 Subject: [PATCH 24/86] Add media type and filename properties on Resource --- .../r2/shared/util/asset/AssetRetriever.kt | 14 ++++--- .../util/asset/ContentResourceFactory.kt | 31 ++++++++++---- .../shared/util/asset/FileResourceFactory.kt | 24 +++++++---- .../shared/util/asset/HttpResourceFactory.kt | 6 ++- .../shared/util/downloads/DownloadManager.kt | 2 +- .../r2/shared/util/http/DefaultHttpClient.kt | 2 +- .../r2/shared/util/http/HttpContainer.kt | 8 ++-- .../r2/shared/util/http/HttpResource.kt | 42 +++++++++++++++++-- .../r2/shared/util/http/HttpResponse.kt | 2 +- ...urceAdapters.kt => BlobResourceAdapter.kt} | 29 +++++-------- .../util/resource/DirectoryContainer.kt | 10 +++-- .../r2/shared/util/resource/FileProperties.kt | 39 +++++++++++++++++ .../readium/r2/streamer/ParserAssetFactory.kt | 6 ++- .../readium/r2/streamer/PublicationFactory.kt | 2 +- .../java/org/readium/r2/testapp/Readium.kt | 4 +- 15 files changed, 159 insertions(+), 62 deletions(-) rename readium/shared/src/main/java/org/readium/r2/shared/util/resource/{BlobResourceAdapters.kt => BlobResourceAdapter.kt} (65%) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 3f491afab1..d2162cedc3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -14,13 +14,13 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.archive.ArchiveProvider import org.readium.r2.shared.util.archive.FileZipArchiveProvider -import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.getOrElse 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.invoke import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.tryRecover @@ -171,20 +171,22 @@ public class AssetRetriever( ) } - // FIXME: should use HTTP Content-Type but not the resource content + val properties = resource.properties() + .getOrElse { return Try.failure(Error.ReadError(it)) } + val containerType = archiveProvider.sniffHints( - MediaTypeHints(fileExtension = url.extension) + MediaTypeHints(properties) ) .tryRecover { archiveProvider.sniffBlob(resource) - }.tryRecover { error -> + }.getOrElse { error -> when (error) { MediaTypeSnifferError.NotRecognized -> - Try.success(null) + null is MediaTypeSnifferError.Read -> return Try.failure(Error.ReadError(error.cause)) } - }.assertSuccess() + } if (containerType == null) { val mediaType = resource.mediaType() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt index 4374360bd4..480370d575 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt @@ -7,15 +7,17 @@ package org.readium.r2.shared.util.asset import android.content.ContentResolver +import android.provider.MediaStore +import org.readium.r2.shared.extensions.queryProjection import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ContentBlob 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 org.readium.r2.shared.util.resource.GuessMediaTypeResourceAdapter -import org.readium.r2.shared.util.resource.KnownMediaTypeResourceAdapter +import org.readium.r2.shared.util.resource.BlobResourceAdapter import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.resource.mediaType import org.readium.r2.shared.util.toUri /** @@ -23,7 +25,7 @@ import org.readium.r2.shared.util.toUri */ public class ContentResourceFactory( private val contentResolver: ContentResolver, - private val mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(contentResolver) + private val mediaTypeRetriever: MediaTypeRetriever ) : ResourceFactory { override suspend fun create( @@ -36,12 +38,23 @@ public class ContentResourceFactory( val blob = ContentBlob(url.toUri(), contentResolver) - val resource = mediaType - ?.let { KnownMediaTypeResourceAdapter(blob, it) } - ?: GuessMediaTypeResourceAdapter( + val filename = + contentResolver.queryProjection(url.uri, MediaStore.MediaColumns.DISPLAY_NAME) + + val properties = + Resource.Properties( + Resource.Properties.Builder() + .also { + it.filename = filename + it.mediaType = mediaType + } + ) + + val resource = + BlobResourceAdapter( blob, - mediaTypeRetriever, - MediaTypeHints(fileExtension = url.extension) + properties, + mediaTypeRetriever ) return Try.success(resource) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt index 8d99a84773..f978990628 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt @@ -10,11 +10,11 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.FileBlob 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 org.readium.r2.shared.util.resource.GuessMediaTypeResourceAdapter -import org.readium.r2.shared.util.resource.KnownMediaTypeResourceAdapter +import org.readium.r2.shared.util.resource.BlobResourceAdapter import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.resource.mediaType public class FileResourceFactory( private val mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever() @@ -29,12 +29,20 @@ public class FileResourceFactory( val blob = FileBlob(file) - val resource = mediaType - ?.let { KnownMediaTypeResourceAdapter(blob, it) } - ?: GuessMediaTypeResourceAdapter( + val properties = + Resource.Properties( + Resource.Properties.Builder() + .also { + it.filename = url.filename + it.mediaType = mediaType + } + ) + + val resource = + BlobResourceAdapter( blob, - mediaTypeRetriever, - MediaTypeHints(fileExtension = file.extension) + properties, + mediaTypeRetriever ) return Try.success(resource) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt index 50070712e5..2dd6a3b3b7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt @@ -11,10 +11,12 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpResource import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource public class HttpResourceFactory( - private val httpClient: HttpClient + private val httpClient: HttpClient, + private val mediaTypeRetriever: MediaTypeRetriever ) : ResourceFactory { override suspend fun create( @@ -25,7 +27,7 @@ public class HttpResourceFactory( return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) } - val resource = HttpResource(httpClient, url) + val resource = HttpResource(httpClient, url, mediaTypeRetriever) return Try.success(resource) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt index e9b1b129cd..4fd85ae7c6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt @@ -30,7 +30,7 @@ public interface DownloadManager { public data class Download( val file: File, - val mediaType: MediaType + val mediaType: MediaType? ) @JvmInline diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index 5093b11d35..7003f8d054 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -194,7 +194,7 @@ public class DefaultHttpClient( url = request.url, statusCode = statusCode, headers = connection.safeHeaders, - mediaType = mediaType ?: MediaType.BINARY + mediaType = mediaType ) callback.onResponseReceived(request, response) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt index da21974c47..ec9631f055 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt @@ -9,6 +9,7 @@ package org.readium.r2.shared.util.http import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource /** @@ -21,9 +22,10 @@ import org.readium.r2.shared.util.resource.Resource * @param baseUrl Base URL from which relative URLs are served. */ public class HttpContainer( - private val client: HttpClient, private val baseUrl: Url? = null, - override val entries: Set + override val entries: Set, + private val client: HttpClient, + private val mediaTypeRetriever: MediaTypeRetriever ) : Container { override fun get(url: Url): Resource? { @@ -34,7 +36,7 @@ public class HttpContainer( return if (absoluteUrl == null || !absoluteUrl.isHttp) { null } else { - HttpResource(client, absoluteUrl) + HttpResource(client, absoluteUrl, mediaTypeRetriever) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index 7eb34c6105..212a70aa73 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -1,3 +1,9 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + package org.readium.r2.shared.util.http import java.io.IOException @@ -12,23 +18,53 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.invoke +import org.readium.r2.shared.util.resource.mediaType +import org.readium.r2.shared.util.tryRecover /** Provides access to an external URL through HTTP. */ @OptIn(ExperimentalReadiumApi::class) public class HttpResource( private val client: HttpClient, override val source: AbsoluteUrl, + private val mediaTypeRetriever: MediaTypeRetriever, private val maxSkipBytes: Long = MAX_SKIP_BYTES ) : Resource { - override suspend fun mediaType(): Try = - headResponse().map { it.mediaType } + override suspend fun mediaType(): Try { + val properties = properties() + .getOrElse { return Try.failure(it) } + + val mediaTypeHints = + MediaTypeHints(properties) + + return mediaTypeRetriever.retrieve(mediaTypeHints, this) + .tryRecover { + when (it) { + MediaTypeSnifferError.NotRecognized -> + Try.success(MediaType.BINARY) + is MediaTypeSnifferError.Read -> + Try.failure(it.cause) + } + } + } override suspend fun properties(): Try = - Try.success(Resource.Properties()) + headResponse().map { + Resource.Properties( + Resource.Properties.Builder() + .apply { + mediaType = it.mediaType + } + ) + } override suspend fun length(): Try = headResponse().flatMap { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt index 72e8d5a215..c57a92afd6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt @@ -24,7 +24,7 @@ public data class HttpResponse( val url: AbsoluteUrl, val statusCode: Int, val headers: Map>, - val mediaType: MediaType + val mediaType: MediaType? ) { private val httpHeaders = HttpHeaders(headers) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt similarity index 65% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt index ef14ffc0b5..67b4033692 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapters.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt @@ -15,28 +15,18 @@ import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.tryRecover -internal class KnownMediaTypeResourceAdapter( +internal class BlobResourceAdapter( private val blob: Blob, - private val mediaType: MediaType + properties: Resource.Properties, + private val mediaTypeRetriever: MediaTypeRetriever ) : Resource, Blob by blob { - override suspend fun mediaType(): Try = - Try.success(mediaType) - - override suspend fun properties(): Try { - return Try.success(Resource.Properties()) - } -} - -internal class GuessMediaTypeResourceAdapter( - private val blob: Blob, - private val mediaTypeRetriever: MediaTypeRetriever, - private val mediaTypeHints: MediaTypeHints -) : Resource, Blob by blob { + private val properties: Resource.Properties = + properties.copy { mediaType = mediaTypeRetriever.retrieve(MediaTypeHints(properties)) } override suspend fun mediaType(): Try = mediaTypeRetriever.retrieve( - hints = mediaTypeHints, + hints = MediaTypeHints(properties), blob = blob ).tryRecover { error -> when (error) { @@ -47,7 +37,8 @@ internal class GuessMediaTypeResourceAdapter( } } - override suspend fun properties(): Try { - return Try.success(Resource.Properties()) - } + override suspend fun properties(): Try = + Try.success( + Resource.Properties(properties) + ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index 53a58f43f0..449612e04d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -15,7 +15,6 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.data.FileSystemError -import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.toUrl @@ -29,10 +28,13 @@ public class DirectoryContainer( ) : Container { private fun File.toResource(): Resource { - return GuessMediaTypeResourceAdapter( + return BlobResourceAdapter( FileBlob(this), - mediaTypeRetriever, - MediaTypeHints(fileExtension = extension) + Resource.Properties( + Resource.Properties.Builder() + .also { it.filename = name } + ), + mediaTypeRetriever ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt new file mode 100644 index 0000000000..0553b27899 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt @@ -0,0 +1,39 @@ +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeHints + +private const val FILENAME_KEY = "filename" + +private const val MEDIA_TYPE_KEY = "mediaType" + +public val Resource.Properties.filename: String? + get() = this[FILENAME_KEY] as? String + +public var Resource.Properties.Builder.filename: String? + get() = this[FILENAME_KEY] as? String? + set(value) { + if (value == null) { + remove(FILENAME_KEY) + } else { + put(FILENAME_KEY, value) + } + } + +public val Resource.Properties.mediaType: MediaType? + get() = (this[MEDIA_TYPE_KEY] as? String?) + ?.let { MediaType(it) } + +public var Resource.Properties.Builder.mediaType: MediaType? + get() = (this[MEDIA_TYPE_KEY] as? String?) + ?.let { MediaType(it) } + set(value) { + if (value == null) { + remove(MEDIA_TYPE_KEY) + } else { + put(FILENAME_KEY, value.toString()) + } + } + +public operator fun MediaTypeHints.Companion.invoke(properties: Resource.Properties): MediaTypeHints = + MediaTypeHints(mediaType = properties.mediaType) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index e3c74eca96..31c50e3604 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -21,13 +21,15 @@ import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpContainer import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.streamer.parser.PublicationParser import timber.log.Timber internal class ParserAssetFactory( private val httpClient: HttpClient, - private val formatRegistry: FormatRegistry + private val formatRegistry: FormatRegistry, + private val mediaTypeRetriever: MediaTypeRetriever ) { sealed class Error( @@ -116,7 +118,7 @@ internal class ParserAssetFactory( Url("manifest.json")!!, asset.resource ), - HttpContainer(httpClient, baseUrl, resources) + HttpContainer(baseUrl, resources, httpClient, mediaTypeRetriever) ) return Try.success( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index d8dc8faf2f..9125cda8dd 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -114,7 +114,7 @@ public class PublicationFactory( if (!ignoreDefaultParsers) defaultParsers else emptyList() private val parserAssetFactory: ParserAssetFactory = - ParserAssetFactory(httpClient, formatRegistry) + ParserAssetFactory(httpClient, formatRegistry, mediaTypeRetriever) /** * Opens a [Publication] from the given asset. diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index d0527b42eb..4a04504aef 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -47,8 +47,8 @@ class Readium(context: Context) { private val resourceFactory = CompositeResourceFactory( FileResourceFactory(mediaTypeRetriever), - ContentResourceFactory(context.contentResolver), - HttpResourceFactory(httpClient) + ContentResourceFactory(context.contentResolver, mediaTypeRetriever), + HttpResourceFactory(httpClient, mediaTypeRetriever) ) val assetRetriever = AssetRetriever( From 3c77ec185afbc6104a47cf9235f6580d5e281a2e Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Tue, 21 Nov 2023 12:02:32 +0100 Subject: [PATCH 25/86] Fix --- .../org/readium/r2/shared/util/asset/HttpResourceFactory.kt | 2 +- .../main/java/org/readium/r2/shared/util/http/HttpContainer.kt | 2 +- .../main/java/org/readium/r2/shared/util/http/HttpResource.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt index 2dd6a3b3b7..62e5d24ba6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt @@ -27,7 +27,7 @@ public class HttpResourceFactory( return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) } - val resource = HttpResource(httpClient, url, mediaTypeRetriever) + val resource = HttpResource(url, httpClient, mediaTypeRetriever) return Try.success(resource) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt index ec9631f055..d7f24ee940 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt @@ -36,7 +36,7 @@ public class HttpContainer( return if (absoluteUrl == null || !absoluteUrl.isHttp) { null } else { - HttpResource(client, absoluteUrl, mediaTypeRetriever) + HttpResource(absoluteUrl, client, mediaTypeRetriever) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index 212a70aa73..4f190ecd86 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -32,8 +32,8 @@ import org.readium.r2.shared.util.tryRecover /** Provides access to an external URL through HTTP. */ @OptIn(ExperimentalReadiumApi::class) public class HttpResource( - private val client: HttpClient, override val source: AbsoluteUrl, + private val client: HttpClient, private val mediaTypeRetriever: MediaTypeRetriever, private val maxSkipBytes: Long = MAX_SKIP_BYTES ) : Resource { From 6b200b1cc05d6fd47585b7016ce68ac8114d9f42 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Thu, 23 Nov 2023 09:16:24 +0100 Subject: [PATCH 26/86] Remove MediaTypeRetriever from EpubParser --- .../java/org/readium/r2/streamer/PublicationFactory.kt | 2 +- .../java/org/readium/r2/streamer/parser/epub/EpubParser.kt | 7 ++----- .../org/readium/r2/streamer/parser/epub/ManifestAdapter.kt | 7 ++----- .../org/readium/r2/streamer/parser/epub/MetadataParser.kt | 6 ++---- .../org/readium/r2/streamer/parser/epub/PackageDocument.kt | 5 ++--- .../org/readium/r2/streamer/parser/epub/ResourceAdapter.kt | 7 +++---- 6 files changed, 12 insertions(+), 22 deletions(-) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 9125cda8dd..109745b506 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -103,7 +103,7 @@ public class PublicationFactory( private val defaultParsers: List = listOfNotNull( - EpubParser(mediaTypeRetriever), + EpubParser(), pdfFactory?.let { PdfParser(context, it) }, ReadiumWebPubParser(context, pdfFactory), ImageParser(), diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 2be5b1e595..b0075fb128 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -24,7 +24,6 @@ import org.readium.r2.shared.util.data.readAsXml import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.TransformingContainer import org.readium.r2.shared.util.use @@ -40,7 +39,6 @@ import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref */ @OptIn(ExperimentalReadiumApi::class) public class EpubParser( - private val mediaTypeRetriever: MediaTypeRetriever, private val reflowablePositionsStrategy: EpubPositionsService.ReflowableStrategy = EpubPositionsService.ReflowableStrategy.recommended ) : PublicationParser { @@ -65,7 +63,7 @@ public class EpubParser( val opfXmlDocument = opfResource .use { it.decodeOrFail(opfPath) { readAsXml() } } .getOrElse { return Try.failure(it) } - val packageDocument = PackageDocument.parse(opfXmlDocument, opfPath, mediaTypeRetriever) + val packageDocument = PackageDocument.parse(opfXmlDocument, opfPath) ?: return Try.failure( PublicationParser.Error.ReadError( ReadError.Decoding( @@ -78,8 +76,7 @@ public class EpubParser( packageDocument = packageDocument, navigationData = parseNavigationData(packageDocument, asset.container), encryptionData = parseEncryptionData(asset.container), - displayOptions = parseDisplayOptions(asset.container), - mediaTypeRetriever = mediaTypeRetriever + displayOptions = parseDisplayOptions(asset.container) ).adapt() var container = asset.container diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ManifestAdapter.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ManifestAdapter.kt index 4b578d6e55..fe1fe9012c 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ManifestAdapter.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ManifestAdapter.kt @@ -11,7 +11,6 @@ import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.PublicationCollection import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever /** * Creates a [Manifest] model from an EPUB package's document. @@ -23,8 +22,7 @@ internal class ManifestAdapter( private val packageDocument: PackageDocument, private val navigationData: Map> = emptyMap(), private val encryptionData: Map = emptyMap(), - private val displayOptions: Map = emptyMap(), - private val mediaTypeRetriever: MediaTypeRetriever + private val displayOptions: Map = emptyMap() ) { private val epubVersion = packageDocument.epubVersion private val spine = packageDocument.spine @@ -44,8 +42,7 @@ internal class ManifestAdapter( packageDocument.manifest, encryptionData, metadata.coverId, - metadata.durationById, - mediaTypeRetriever = mediaTypeRetriever + metadata.durationById ).adapt() // Compute toc and otherCollections diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataParser.kt index 568e345d88..d85fbb9bc2 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataParser.kt @@ -9,13 +9,11 @@ package org.readium.r2.streamer.parser.epub import org.readium.r2.shared.publication.Href import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.xml.ElementNode import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref internal class MetadataParser( - private val prefixMap: Map, - private val mediaTypeRetriever: MediaTypeRetriever + private val prefixMap: Map ) { fun parse(document: ElementNode, filePath: Url): List? { @@ -57,7 +55,7 @@ internal class MetadataParser( refines = refines, href = Href(filePath.resolve(href)), rels = rel.toSet(), - mediaType = mediaTypeRetriever.retrieve(mediaType), + mediaType = mediaType?.let { MediaType(it) }, properties = properties ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt index e193c91102..0e3afb4dd7 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/PackageDocument.kt @@ -8,7 +8,6 @@ package org.readium.r2.streamer.parser.epub import org.readium.r2.shared.publication.ReadingProgression import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.xml.ElementNode import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref @@ -22,11 +21,11 @@ internal data class PackageDocument( ) { companion object { - fun parse(document: ElementNode, filePath: Url, mediaTypeRetriever: MediaTypeRetriever): PackageDocument? { + fun parse(document: ElementNode, filePath: Url): PackageDocument? { val packagePrefixes = document.getAttr("prefix")?.let { parsePrefixes(it) }.orEmpty() val prefixMap = PACKAGE_RESERVED_PREFIXES + packagePrefixes // prefix element overrides reserved prefixes val epubVersion = document.getAttr("version")?.toDoubleOrNull() ?: 1.2 - val metadata = MetadataParser(prefixMap, mediaTypeRetriever).parse(document, filePath) + val metadata = MetadataParser(prefixMap).parse(document, filePath) ?: return null val manifestElement = document.getFirst("manifest", Namespaces.OPF) ?: return null diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ResourceAdapter.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ResourceAdapter.kt index d1870c12d3..53efbea449 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ResourceAdapter.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/ResourceAdapter.kt @@ -12,15 +12,14 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Properties import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.mediatype.MediaType internal class ResourceAdapter( private val spine: Spine, private val manifest: List, private val encryptionData: Map, private val coverId: String?, - private val durationById: Map, - private val mediaTypeRetriever: MediaTypeRetriever + private val durationById: Map ) { data class Links( val readingOrder: List, @@ -73,7 +72,7 @@ internal class ResourceAdapter( return Link( href = Href(item.href), - mediaType = mediaTypeRetriever.retrieve(item.mediaType), + mediaType = item.mediaType?.let { MediaType(it) }, duration = durationById[item.id], rels = rels, properties = properties, From efb567163f13075ad3e0de0d35f6b1f9dc70675f Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Thu, 23 Nov 2023 09:43:23 +0100 Subject: [PATCH 27/86] Clarify and move MediaTypeRetriever --- .../readium/r2/lcp/LcpContentProtection.kt | 2 +- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 2 +- .../readium/r2/lcp/LcpPublicationRetriever.kt | 2 +- .../java/org/readium/r2/lcp/LcpService.kt | 2 +- .../r2/lcp/license/model/components/Link.kt | 2 +- .../readium/r2/lcp/service/LicensesService.kt | 2 +- .../readium/r2/lcp/service/NetworkService.kt | 2 +- .../java/org/readium/r2/opds/OPDS1Parser.kt | 2 +- .../java/org/readium/r2/opds/OPDS2Parser.kt | 2 +- .../util/archive/FileZipArchiveProvider.kt | 2 +- .../shared/util/archive/FileZipContainer.kt | 2 +- .../r2/shared/util/asset/AssetRetriever.kt | 2 +- .../util/asset/ContentResourceFactory.kt | 2 +- .../shared/util/asset/FileResourceFactory.kt | 2 +- .../shared/util/asset/HttpResourceFactory.kt | 2 +- .../android/AndroidDownloadManager.kt | 2 +- .../r2/shared/util/http/DefaultHttpClient.kt | 2 +- .../r2/shared/util/http/HttpContainer.kt | 2 +- .../r2/shared/util/http/HttpResource.kt | 2 +- .../util/resource/BlobResourceAdapter.kt | 1 - .../util/resource/DirectoryContainer.kt | 1 - .../MediaTypeRetriever.kt | 62 +++++++++---------- .../r2/shared/util/zip/ChannelZipContainer.kt | 2 +- .../util/zip/StreamingZipArchiveProvider.kt | 2 +- .../LcpFallbackContentProtectionTest.kt | 2 +- .../util/mediatype/MediaTypeRetrieverTest.kt | 1 + .../util/resource/DirectoryContainerTest.kt | 1 - .../shared/util/resource/ZipContainerTest.kt | 1 - .../readium/r2/streamer/ParserAssetFactory.kt | 2 +- .../readium/r2/streamer/PublicationFactory.kt | 2 +- .../parser/epub/EpubDeobfuscatorTest.kt | 2 +- .../parser/epub/PackageDocumentTest.kt | 2 +- .../streamer/parser/image/ImageParserTest.kt | 2 +- .../java/org/readium/r2/testapp/Readium.kt | 2 +- 34 files changed, 60 insertions(+), 63 deletions(-) rename readium/shared/src/main/java/org/readium/r2/shared/util/{mediatype => resource}/MediaTypeRetriever.kt (79%) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index 5fc15009dc..f8a7b45fb8 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -22,7 +22,7 @@ import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.TransformingContainer internal class LcpContentProtection( diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 71ff089045..57f17e90c1 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -22,9 +22,9 @@ import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.FailureResource +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.TransformingResource import org.readium.r2.shared.util.resource.flatMap diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt index 6695d0c21d..eadaee4cac 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt @@ -19,7 +19,7 @@ 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 +import org.readium.r2.shared.util.resource.MediaTypeRetriever /** * Utility to acquire a protected publication from an LCP License Document. diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt index 7cd9932196..877753e172 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt @@ -32,7 +32,7 @@ import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.resource.MediaTypeRetriever /** * Service used to acquire and open publications protected with LCP. diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt index cb0cbe62a3..aed3a404cc 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt @@ -19,7 +19,7 @@ import org.readium.r2.shared.extensions.optStringsFromArrayOrSingle import org.readium.r2.shared.publication.Href import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.resource.MediaTypeRetriever public data class Link( val href: Href, diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index f913117a97..87046bb640 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -41,7 +41,7 @@ import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.resource.MediaTypeRetriever import timber.log.Timber internal class LicensesService( diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt index 76ea7188d0..698c9ee654 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt @@ -26,7 +26,7 @@ import org.readium.r2.shared.util.Url 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 org.readium.r2.shared.util.resource.MediaTypeRetriever import timber.log.Timber internal typealias URLParameters = Map diff --git a/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt b/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt index 39e62c935c..f84b408525 100644 --- a/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt +++ b/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt @@ -24,7 +24,7 @@ import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.fetchWithDecoder import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.xml.ElementNode import org.readium.r2.shared.util.xml.XmlParser diff --git a/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt b/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt index 5afea4d5fe..7d2f5c5524 100644 --- a/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt +++ b/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt @@ -32,7 +32,7 @@ import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.fetchWithDecoder -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.resource.MediaTypeRetriever public enum class OPDS2ParserError { MetadataNotFound, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt index f842496f31..c3917fc4ef 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt @@ -22,8 +22,8 @@ import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt index f5d618e8d6..7a256d4a57 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt @@ -27,8 +27,8 @@ import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.tryRecover diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index d2162cedc3..d5714516ac 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -17,8 +17,8 @@ import org.readium.r2.shared.util.archive.FileZipArchiveProvider import org.readium.r2.shared.util.getOrElse 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.invoke import org.readium.r2.shared.util.toUrl diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt index 480370d575..dc17d9f88e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt @@ -13,8 +13,8 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ContentBlob import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.BlobResourceAdapter +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.filename import org.readium.r2.shared.util.resource.mediaType diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt index f978990628..0083093805 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt @@ -10,8 +10,8 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.BlobResourceAdapter +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.filename import org.readium.r2.shared.util.resource.mediaType diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt index 62e5d24ba6..f52b33caa5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt @@ -11,7 +11,7 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpResource import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource public class HttpResourceFactory( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index 474087916a..141bdaac0e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -34,7 +34,7 @@ import org.readium.r2.shared.util.http.HttpStatus 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 +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.toUri import org.readium.r2.shared.util.units.Hz import org.readium.r2.shared.util.units.hz diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index 7003f8d054..4562aad694 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -32,7 +32,7 @@ import org.readium.r2.shared.util.getOrDefault import org.readium.r2.shared.util.http.HttpRequest.Method 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 org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.toDebugDescription import org.readium.r2.shared.util.tryRecover import timber.log.Timber diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt index d7f24ee940..571393fdab 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt @@ -9,7 +9,7 @@ package org.readium.r2.shared.util.http import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index 4f190ecd86..d9a9889093 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -22,8 +22,8 @@ import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.invoke import org.readium.r2.shared.util.resource.mediaType diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt index 67b4033692..63a00cd3d5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt @@ -11,7 +11,6 @@ import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.ReadError 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.tryRecover diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index 449612e04d..bf6b54709d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -15,7 +15,6 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.data.FileSystemError -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.toUrl /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt similarity index 79% rename from readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt index 9ba9ba0e49..5f72c23ec3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.mediatype +package org.readium.r2.shared.util.resource import android.content.ContentResolver import android.provider.MediaStore @@ -14,6 +14,12 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.FileBlob +import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeHints +import org.readium.r2.shared.util.mediatype.MediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.mediatype.SystemMediaTypeSniffer import org.readium.r2.shared.util.toUri /** @@ -75,22 +81,20 @@ public class MediaTypeRetriever( public suspend fun retrieve( hints: MediaTypeHints = MediaTypeHints(), - container: Container<*>? = null + container: Container<*> ): Try { mediaTypeSniffer.sniffHints(hints) .getOrNull() ?.let { return Try.success(it) } - if (container != null) { - mediaTypeSniffer.sniffContainer(container) - .onSuccess { return Try.success(it) } - .onFailure { error -> - when (error) { - is MediaTypeSnifferError.NotRecognized -> {} - else -> return Try.failure(error) - } + mediaTypeSniffer.sniffContainer(container) + .onSuccess { return Try.success(it) } + .onFailure { error -> + when (error) { + is MediaTypeSnifferError.NotRecognized -> {} + else -> return Try.failure(error) } - } + } return hints.mediaTypes.firstOrNull() ?.let { Try.success(it) } @@ -109,22 +113,20 @@ public class MediaTypeRetriever( */ public suspend fun retrieve( hints: MediaTypeHints = MediaTypeHints(), - blob: Blob? = null + blob: Blob ): Try { mediaTypeSniffer.sniffHints(hints) .getOrNull() ?.let { return Try.success(it) } - if (blob != null) { - mediaTypeSniffer.sniffBlob(blob) - .onSuccess { return Try.success(it) } - .onFailure { error -> - when (error) { - is MediaTypeSnifferError.NotRecognized -> {} - else -> return Try.failure(error) - } + mediaTypeSniffer.sniffBlob(blob) + .onSuccess { return Try.success(it) } + .onFailure { error -> + when (error) { + is MediaTypeSnifferError.NotRecognized -> {} + else -> return Try.failure(error) } - } + } // Falls back on the system-wide registered media types using MimeTypeMap. // Note: This is done after the default sniffers, because otherwise it will detect @@ -134,16 +136,14 @@ public class MediaTypeRetriever( .getOrNull() ?.let { return Try.success(it) } - if (blob != null) { - SystemMediaTypeSniffer.sniffBlob(blob) - .onSuccess { return Try.success(it) } - .onFailure { error -> - when (error) { - is MediaTypeSnifferError.NotRecognized -> {} - else -> return Try.failure(error) - } + SystemMediaTypeSniffer.sniffBlob(blob) + .onSuccess { return Try.success(it) } + .onFailure { error -> + when (error) { + is MediaTypeSnifferError.NotRecognized -> {} + else -> return Try.failure(error) } - } + } // Falls back on the [contentResolver] in case of content Uri. // Note: This is done after the heavy sniffing of the provided [sniffers], because @@ -151,7 +151,7 @@ public class MediaTypeRetriever( // their content (for example, for RWPM). if (contentResolver != null) { - blob?.source + blob.source ?.takeIf { it.isContent } ?.let { url -> val contentHints = MediaTypeHints( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt index 99110b5d2e..465ede97cc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt @@ -25,8 +25,8 @@ import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.tryRecover diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 23c564a8fe..390f0bf97b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -22,8 +22,8 @@ import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toUrl diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt index c0c3d4505b..2ec5321cf0 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt @@ -13,7 +13,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt index 1cf3efe29d..af2dc6bec6 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt @@ -11,6 +11,7 @@ import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.archive.FileZipArchiveProvider +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt index 8f6be1d7d1..8f54c3d55a 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt @@ -22,7 +22,6 @@ import org.readium.r2.shared.lengthBlocking import org.readium.r2.shared.readBlocking import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.toAbsoluteUrl import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt index b2db991e05..3b140034c0 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -23,7 +23,6 @@ import org.readium.r2.shared.util.archive.FileZipArchiveProvider import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.use import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider import org.robolectric.ParameterizedRobolectricTestRunner diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index 31c50e3604..c25f3ff53a 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -21,7 +21,7 @@ import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpContainer import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.streamer.parser.PublicationParser import timber.log.Timber diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 109745b506..4ceb83e67a 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -20,8 +20,8 @@ import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.FormatRegistry -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.pdf.PdfDocumentFactory +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.streamer.parser.PublicationParser import org.readium.r2.streamer.parser.audio.AudioParser import org.readium.r2.streamer.parser.epub.EpubParser diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt index eea8bf466e..15b6668b24 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt @@ -16,8 +16,8 @@ import org.junit.runner.RunWith import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.assertSuccess -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.DirectoryContainerFactory +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.flatMap import org.readium.r2.shared.util.toAbsoluteUrl diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt index ce62a39dbe..5fca2b64a9 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt @@ -22,7 +22,7 @@ import org.readium.r2.shared.publication.epub.layout import org.readium.r2.shared.publication.presentation.* import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.xml.XmlParser import org.robolectric.RobolectricTestRunner diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index cfc0a11eeb..a834a59a32 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -22,7 +22,7 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.archive.FileZipArchiveProvider import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toUrl import org.readium.r2.streamer.parseBlocking diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 4a04504aef..28f797bead 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -25,7 +25,7 @@ import org.readium.r2.shared.util.asset.HttpResourceFactory import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.mediatype.FormatRegistry -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider import org.readium.r2.streamer.PublicationFactory From 1d855165797e3f377e48db93c9d875602aadd2a1 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Thu, 23 Nov 2023 13:25:21 +0100 Subject: [PATCH 28/86] Refactor ArchiveFactory --- .../readium/r2/lcp/LcpContentProtection.kt | 2 +- .../util/archive/ZipHintMediaTypeSniffer.kt | 52 ------- .../org/readium/r2/shared/util/asset/Asset.kt | 1 - .../r2/shared/util/asset/AssetRetriever.kt | 140 +++++------------- .../shared/util/mediatype/FormatRegistry.kt | 54 ++++++- .../shared/util/mediatype/MediaTypeSniffer.kt | 4 +- .../ArchiveFactory.kt} | 36 +---- .../ArchiveProperties.kt | 3 +- .../util/resource/MediaTypeRetriever.kt | 27 +++- .../util/resource/SmartArchiveFactory.kt | 40 +++++ .../FileZipArchiveProvider.kt | 25 ++-- .../util/{archive => zip}/FileZipContainer.kt | 4 +- .../util/zip/StreamingZipArchiveProvider.kt | 33 +++-- ...pContainer.kt => StreamingZipContainer.kt} | 6 +- .../r2/shared/util/zip/ZipArchiveFactory.kt | 30 ++++ .../r2/shared/util/zip/ZipMediaTypeSniffer.kt | 34 +++++ .../util/mediatype/MediaTypeRetrieverTest.kt | 2 +- .../r2/shared/util/resource/PropertiesTest.kt | 2 - .../shared/util/resource/ZipContainerTest.kt | 2 +- .../parser/epub/EpubPositionsService.kt | 2 +- .../parser/epub/EpubPositionsServiceTest.kt | 4 +- .../streamer/parser/image/ImageParserTest.kt | 2 +- .../r2/testapp/domain/PublicationError.kt | 2 +- 23 files changed, 276 insertions(+), 231 deletions(-) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipHintMediaTypeSniffer.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/{archive/ArchiveProvider.kt => resource/ArchiveFactory.kt} (65%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{archive => resource}/ArchiveProperties.kt (95%) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/{archive => zip}/FileZipArchiveProvider.kt (86%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{archive => zip}/FileZipContainer.kt (97%) rename readium/shared/src/main/java/org/readium/r2/shared/util/zip/{ChannelZipContainer.kt => StreamingZipContainer.kt} (97%) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index f8a7b45fb8..cbed453463 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -176,7 +176,7 @@ internal class LcpContentProtection( private fun AssetRetriever.Error.wrap(): ContentProtection.Error = when (this) { - is AssetRetriever.Error.ArchiveFormatNotSupported -> + is AssetRetriever.Error.FormatNotSupported -> ContentProtection.Error.UnsupportedAsset(this) is AssetRetriever.Error.ReadError -> ContentProtection.Error.ReadError(cause) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipHintMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipHintMediaTypeSniffer.kt deleted file mode 100644 index f60ddaf595..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ZipHintMediaTypeSniffer.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.archive - -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.HintMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints -import org.readium.r2.shared.util.mediatype.MediaTypeSniffer -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError - -internal object ZipHintMediaTypeSniffer : HintMediaTypeSniffer { - - private val generalSniffer: MediaTypeSniffer = - DefaultMediaTypeSniffer() - - private val acceptedMediaTypes: List = - listOf( - MediaType.EPUB, - MediaType.READIUM_WEBPUB, - MediaType.READIUM_AUDIOBOOK, - MediaType.DIVINA, - MediaType.LCP_PROTECTED_PDF, - MediaType.LCP_PROTECTED_AUDIOBOOK, - MediaType.LPF, - MediaType.CBZ, - MediaType.ZAB - ) - - override fun sniffHints(hints: MediaTypeHints): Try { - if (hints.hasMediaType("application/zip") || - hints.hasFileExtension("zip") - ) { - return Try.success(MediaType.ZIP) - } - - val mediaType = generalSniffer.sniffHints(hints) - .getOrElse { return Try.failure(it) } - - if (mediaType in acceptedMediaTypes) { - return Try.success(MediaType.ZIP) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt index 128da80298..bec71340e3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt @@ -48,7 +48,6 @@ public sealed class Asset { */ public class Container( override val mediaType: MediaType, - public val containerType: MediaType, public val container: org.readium.r2.shared.util.data.Container ) : Asset() { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index d5714516ac..4558b17ceb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -11,18 +11,17 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.archive.ArchiveFactory -import org.readium.r2.shared.util.archive.ArchiveProvider -import org.readium.r2.shared.util.archive.FileZipArchiveProvider import org.readium.r2.shared.util.getOrElse +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.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.ArchiveFactory import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.SmartArchiveFactory import org.readium.r2.shared.util.resource.invoke import org.readium.r2.shared.util.toUrl -import org.readium.r2.shared.util.tryRecover +import org.readium.r2.shared.util.zip.ZipArchiveFactory /** * Retrieves an [Asset] instance providing reading access to the resource(s) of an asset stored at a @@ -30,8 +29,9 @@ import org.readium.r2.shared.util.tryRecover */ public class AssetRetriever( private val resourceFactory: ResourceFactory = FileResourceFactory(), - private val archiveProvider: ArchiveProvider = FileZipArchiveProvider(), - private val mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever() + archiveFactory: ArchiveFactory = ZipArchiveFactory(), + private val mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), + formatRegistry: FormatRegistry = FormatRegistry() ) { public sealed class Error( @@ -44,96 +44,39 @@ public class AssetRetriever( cause: org.readium.r2.shared.util.Error? = null ) : Error("Scheme $scheme is not supported.", cause) - public class ArchiveFormatNotSupported(cause: org.readium.r2.shared.util.Error) : + public class FormatNotSupported(cause: org.readium.r2.shared.util.Error) : Error("Archive providers do not support this kind of archive.", cause) public class ReadError(override val cause: org.readium.r2.shared.util.data.ReadError) : Error("An error occurred when trying to read asset.", cause) } + private val archiveFactory: ArchiveFactory = + SmartArchiveFactory(archiveFactory, formatRegistry) + /** * Retrieves an asset from a known media and asset type. */ public suspend fun retrieve( url: AbsoluteUrl, - mediaType: MediaType, - containerType: MediaType? + mediaType: MediaType ): Try { - return when (containerType) { - null -> - retrieveResourceAsset(url, mediaType) - - else -> - retrieveArchiveAsset(url, mediaType, containerType) - } - } - - private suspend fun retrieveArchiveAsset( - url: AbsoluteUrl, - mediaType: MediaType, - containerType: MediaType - ): Try { - val resource = retrieveResource(url, containerType) + val resource = retrieveResource(url, mediaType) .getOrElse { return Try.failure(it) } - archiveProvider.sniffHints(MediaTypeHints(mediaType = containerType)) - .onFailure { - return Try.failure( - Error.ArchiveFormatNotSupported( - MessageError("Container type $containerType not recognized.") - ) - ) - } - - return retrieveArchiveAsset(resource, MediaTypeHints(mediaType = mediaType), containerType) - } - private suspend fun retrieveArchiveAsset( - resource: Resource, - mediaTypeHints: MediaTypeHints, - containerType: MediaType - ): Try { - val container = archiveProvider.create(resource) - .mapFailure { error -> - when (error) { + val archive = archiveFactory.create(mediaType, resource) + .getOrElse { + return when (it) { is ArchiveFactory.Error.ReadError -> - Error.ReadError(error.cause) - else -> - Error.ArchiveFormatNotSupported(error) - } - } - .getOrElse { return Try.failure(it) } - - val mediaType = mediaTypeRetriever - .retrieve(mediaTypeHints, container) - .getOrElse { error -> - when (error) { - MediaTypeSnifferError.NotRecognized -> - MediaType.BINARY - is MediaTypeSnifferError.Read -> - return Try.failure(Error.ReadError(error.cause)) + Try.failure(Error.ReadError(it.cause)) + is ArchiveFactory.Error.FormatNotSupported -> + Try.success(Asset.Resource(mediaType, resource)) + is ArchiveFactory.Error.PasswordsNotSupported -> + Try.failure(Error.FormatNotSupported(it)) } } - val asset = Asset.Container( - mediaType = mediaType, - containerType = containerType, - container = container - ) - - return Try.success(asset) - } - - private suspend fun retrieveResourceAsset( - url: AbsoluteUrl, - mediaType: MediaType - ): Try { - return retrieveResource(url, mediaType) - .map { resource -> - Asset.Resource( - mediaType, - resource - ) - } + return Try.success(Asset.Container(mediaType, archive)) } private suspend fun retrieveResource( @@ -174,28 +117,27 @@ public class AssetRetriever( val properties = resource.properties() .getOrElse { return Try.failure(Error.ReadError(it)) } - val containerType = archiveProvider.sniffHints( - MediaTypeHints(properties) - ) - .tryRecover { - archiveProvider.sniffBlob(resource) - }.getOrElse { error -> - when (error) { - MediaTypeSnifferError.NotRecognized -> - null - is MediaTypeSnifferError.Read -> - return Try.failure(Error.ReadError(error.cause)) - } - } - - if (containerType == null) { - val mediaType = resource.mediaType() - .getOrElse { return Try.failure(Error.ReadError(it)) } - return Try.success(Asset.Resource(mediaType, resource)) + val mediaType = mediaTypeRetriever.retrieve( + MediaTypeHints(properties), + resource + ).getOrElse { + return Try.failure( + Error.FormatNotSupported( + MessageError("Cannot determine asset media type.") + ) + ) } - val hints = MediaTypeHints(fileExtension = url.extension) + val container = archiveFactory.create(mediaType, resource) + .getOrElse { + when (it) { + is ArchiveFactory.Error.ReadError -> + return Try.failure(Error.ReadError(it.cause)) + else -> + return Try.success(Asset.Resource(mediaType, resource)) + } + } - return retrieveArchiveAsset(resource, mediaTypeHints = hints, containerType = containerType) + return Try.success(Asset.Container(mediaType, container)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt index 79b19dec25..a369f27c01 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt @@ -26,20 +26,47 @@ public class FormatRegistry( MediaType.READIUM_WEBPUB_MANIFEST to "json", MediaType.W3C_WPUB_MANIFEST to "json", MediaType.ZAB to "zab" - ) + ), + parentMediaTypes: Map = mapOf( + MediaType.EPUB to MediaType.ZIP, + MediaType.READIUM_AUDIOBOOK to MediaType.READIUM_WEBPUB, + MediaType.READIUM_WEBPUB to MediaType.ZIP + ), + archiveMediaTypes: List = listOf(MediaType.ZIP) ) { private val fileExtensions: MutableMap = fileExtensions.toMutableMap() + private val parentMediaTypes: MutableMap = parentMediaTypes.toMutableMap() + + private val archiveMediaTypes = archiveMediaTypes.toMutableList() + /** * Registers a new [fileExtension] for the given [mediaType]. */ - public fun register(mediaType: MediaType, fileExtension: String?) { + public fun register( + mediaType: MediaType, + fileExtension: String?, + isArchive: Boolean, + parent: MediaType? + ) { if (fileExtension == null) { fileExtensions.remove(mediaType) } else { fileExtensions[mediaType] = fileExtension } + + if (parent == null) { + parentMediaTypes.remove(mediaType) + } else { + parentMediaTypes[mediaType] = parent + } + + if (isArchive) { + archiveMediaTypes.add(mediaType) + } else { + archiveMediaTypes.remove(mediaType) + } } /** @@ -47,4 +74,27 @@ public class FormatRegistry( */ public fun fileExtension(mediaType: MediaType): String? = fileExtensions[mediaType] + + public fun parentMediaType(mediaType: MediaType): MediaType? = + parentMediaTypes[mediaType] + + public fun MediaType.isAlso(mediaType: MediaType): Boolean { + if (this == mediaType) { + return true + } + + return parentMediaTypes[this] + ?.isAlso(mediaType) + ?: false + } + + public val MediaType.isArchive: Boolean get() { + if (this in archiveMediaTypes) { + return true + } + + return parentMediaTypes[this] + ?.isArchive + ?: false + } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index 5ce33990e5..6a9bfc5bdb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -96,10 +96,12 @@ public interface MediaTypeSniffer : Try.failure(MediaTypeSnifferError.NotRecognized) } -internal class CompositeMediaTypeSniffer( +public class CompositeMediaTypeSniffer( private val sniffers: List ) : MediaTypeSniffer { + public constructor(vararg sniffers: MediaTypeSniffer) : this(sniffers.toList()) + override fun sniffHints(hints: MediaTypeHints): Try { for (sniffer in sniffers) { sniffer.sniffHints(hints) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt similarity index 65% rename from readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt index 5f86157a7d..98db5e5c00 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt @@ -4,46 +4,17 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.archive +package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.mediatype.CompositeMediaTypeSniffer import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints -import org.readium.r2.shared.util.mediatype.MediaTypeSniffer -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceContainer - -public interface ArchiveProvider : MediaTypeSniffer, ArchiveFactory - -public class CompositeArchiveProvider( - providers: List -) : ArchiveProvider { - - private val archiveFactory = CompositeArchiveFactory(providers) - - private val mediaTypeSniffer = CompositeMediaTypeSniffer(providers) - - override fun sniffHints(hints: MediaTypeHints): Try = - mediaTypeSniffer.sniffHints(hints) - - override suspend fun sniffBlob(blob: Blob): Try = - mediaTypeSniffer.sniffBlob(blob) - override suspend fun create( - blob: Blob, - password: String? - ): Try, ArchiveFactory.Error> = - archiveFactory.create(blob, password) -} /** * A factory to create a [ResourceContainer]s from archive [Blob]s. - * */ public interface ArchiveFactory { @@ -72,6 +43,7 @@ public interface ArchiveFactory { * Creates a new [Container] to access the entries of the given archive. */ public suspend fun create( + mediaType: MediaType, blob: Blob, password: String? = null ): Try, Error> @@ -82,13 +54,13 @@ public class CompositeArchiveFactory( ) : ArchiveFactory { public constructor(vararg factories: ArchiveFactory) : this(factories.toList()) - override suspend fun create( + mediaType: MediaType, blob: Blob, password: String? ): Try, ArchiveFactory.Error> { for (factory in factories) { - factory.create(blob, password) + factory.create(mediaType, blob, password) .getOrElse { error -> when (error) { is ArchiveFactory.Error.FormatNotSupported -> null diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProperties.kt similarity index 95% rename from readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProperties.kt index a1920221bf..677c2c1129 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProperties.kt @@ -4,14 +4,13 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.archive +package org.readium.r2.shared.util.resource import org.json.JSONObject import org.readium.r2.shared.JSONable import org.readium.r2.shared.extensions.optNullableBoolean import org.readium.r2.shared.extensions.optNullableLong import org.readium.r2.shared.extensions.toMap -import org.readium.r2.shared.util.resource.Resource /** * Holds information about how the resource is stored in the archive. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt index 5f72c23ec3..0c1f931d5c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt @@ -14,13 +14,16 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.FileBlob +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer +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.MediaTypeSniffer import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.mediatype.SystemMediaTypeSniffer import org.readium.r2.shared.util.toUri +import org.readium.r2.shared.util.zip.ZipArchiveFactory /** * Retrieves a canonical [MediaType] for the provided media type and file extension hints and/or @@ -31,9 +34,14 @@ import org.readium.r2.shared.util.toUri */ public class MediaTypeRetriever( private val contentResolver: ContentResolver? = null, - private val mediaTypeSniffer: MediaTypeSniffer = DefaultMediaTypeSniffer() + archiveFactory: ArchiveFactory = ZipArchiveFactory(), + private val mediaTypeSniffer: MediaTypeSniffer = DefaultMediaTypeSniffer(), + formatRegistry: FormatRegistry = FormatRegistry() ) : MediaTypeSniffer { + private val archiveFactory: ArchiveFactory = + SmartArchiveFactory(archiveFactory, formatRegistry) + /** * Retrieves a canonical [MediaType] for the provided media type and file extension [hints]. */ @@ -119,6 +127,23 @@ public class MediaTypeRetriever( .getOrNull() ?.let { return Try.success(it) } + val blobMediaType = doSniffBlob(hints, blob) + .getOrElse { return Try.failure(it) } + + val container = archiveFactory.create(blobMediaType, blob) + .getOrElse { + when (it) { + is ArchiveFactory.Error.ReadError -> + return Try.failure(MediaTypeSnifferError.Read(it.cause)) + else -> + return Try.success(blobMediaType) + } + } + + return retrieve(hints, container) + } + + private suspend fun doSniffBlob(hints: MediaTypeHints, blob: Blob): Try { mediaTypeSniffer.sniffBlob(blob) .onSuccess { return Try.success(it) } .onFailure { error -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt new file mode 100644 index 0000000000..b7adbcfb15 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.mediatype.FormatRegistry +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.tryRecover + +internal class SmartArchiveFactory( + private val archiveFactory: ArchiveFactory, + private val formatRegistry: FormatRegistry +) : ArchiveFactory { + + override suspend fun create( + mediaType: MediaType, + blob: Blob, + password: String? + ): Try, ArchiveFactory.Error> = + archiveFactory.create(mediaType, blob, password) + .tryRecover { error -> + when (error) { + is ArchiveFactory.Error.FormatNotSupported -> { + formatRegistry.parentMediaType(mediaType) + ?.let { archiveFactory.create(it, blob, password) } + ?: Try.failure(error) + } + is ArchiveFactory.Error.PasswordsNotSupported -> + Try.failure(error) + is ArchiveFactory.Error.ReadError -> + Try.failure(error) + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt similarity index 86% rename from readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index c3917fc4ef..67eb2b7a0f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.archive +package org.readium.r2.shared.util.zip import java.io.File import java.io.FileNotFoundException @@ -21,22 +21,16 @@ import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.resource.MediaTypeRetriever +import org.readium.r2.shared.util.resource.ArchiveFactory import org.readium.r2.shared.util.resource.Resource /** * An [ArchiveFactory] to open local ZIP files with Java's [ZipFile]. */ -public class FileZipArchiveProvider( - private val mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever() -) : ArchiveProvider { +internal class FileZipArchiveProvider { - override fun sniffHints(hints: MediaTypeHints): Try = - ZipHintMediaTypeSniffer.sniffHints(hints) - - override suspend fun sniffBlob(blob: Blob): Try { + suspend fun sniffBlob(blob: Blob): Try { val file = blob.source?.toFile() ?: return Try.Failure(MediaTypeSnifferError.NotRecognized) @@ -62,10 +56,19 @@ public class FileZipArchiveProvider( } } - override suspend fun create( + suspend fun create( + mediaType: MediaType, blob: Blob, password: String? ): Try, ArchiveFactory.Error> { + if (mediaType != MediaType.ZIP) { + return Try.failure( + ArchiveFactory.Error.FormatNotSupported( + MessageError("Archive type not supported") + ) + ) + } + if (password != null) { return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt similarity index 97% rename from readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt index 7a256d4a57..6b7c83d1b1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/FileZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.archive +package org.readium.r2.shared.util.zip import java.io.File import java.io.IOException @@ -28,8 +28,10 @@ import org.readium.r2.shared.util.io.CountingInputStream import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.ArchiveProperties import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.tryRecover diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 390f0bf97b..8bf3886b4c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -12,18 +12,15 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.unwrapInstance import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.archive.ArchiveFactory -import org.readium.r2.shared.util.archive.ArchiveProvider -import org.readium.r2.shared.util.archive.ZipHintMediaTypeSniffer import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.resource.MediaTypeRetriever +import org.readium.r2.shared.util.resource.ArchiveFactory import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toUrl @@ -34,14 +31,9 @@ import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel * An [ArchiveFactory] able to open a ZIP archive served through a stream (e.g. HTTP server, * content URI, etc.). */ -public class StreamingZipArchiveProvider( - private val mediaTypeRetriever: MediaTypeRetriever -) : ArchiveProvider { +internal class StreamingZipArchiveProvider { - override fun sniffHints(hints: MediaTypeHints): Try = - ZipHintMediaTypeSniffer.sniffHints(hints) - - override suspend fun sniffBlob(blob: Blob): Try { + suspend fun sniffBlob(blob: Blob): Try { return try { openBlob(blob, ::ReadException, null) Try.success(MediaType.ZIP) @@ -55,10 +47,19 @@ public class StreamingZipArchiveProvider( } } - override suspend fun create( + suspend fun create( + mediaType: MediaType, blob: Blob, password: String? ): Try, ArchiveFactory.Error> { + if (mediaType != MediaType.ZIP) { + return Try.failure( + ArchiveFactory.Error.FormatNotSupported( + MessageError("Archive type not supported") + ) + ) + } + if (password != null) { return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) } @@ -88,13 +89,13 @@ public class StreamingZipArchiveProvider( val datasourceChannel = BlobChannel(blob, wrapError) val channel = wrapBaseChannel(datasourceChannel) val zipFile = ZipFile(channel, true) - ChannelZipContainer(zipFile, sourceUrl, mediaTypeRetriever) + StreamingZipContainer(zipFile, sourceUrl, mediaTypeRetriever) } internal suspend fun openFile(file: File): ResourceContainer = withContext(Dispatchers.IO) { val fileChannel = FileChannelAdapter(file, "r") val channel = wrapBaseChannel(fileChannel) - ChannelZipContainer(ZipFile(channel), file.toUrl(), mediaTypeRetriever) + StreamingZipContainer(ZipFile(channel), file.toUrl(), mediaTypeRetriever) } private fun wrapBaseChannel(channel: SeekableByteChannel): SeekableByteChannel { @@ -109,7 +110,7 @@ public class StreamingZipArchiveProvider( } } - public companion object { + companion object { private const val CACHE_ALL_MAX_SIZE = 5242880 diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt similarity index 97% rename from readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt index 465ede97cc..91685dbcfa 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ChannelZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt @@ -16,8 +16,6 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.archive.ArchiveProperties -import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException @@ -26,14 +24,16 @@ import org.readium.r2.shared.util.io.CountingInputStream import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.ArchiveProperties import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry +import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.tryRecover import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipArchiveEntry import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile -internal class ChannelZipContainer( +internal class StreamingZipContainer( private val zipFile: ZipFile, override val source: AbsoluteUrl?, private val mediaTypeRetriever: MediaTypeRetriever diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt new file mode 100644 index 0000000000..503adaace8 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.zip + +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.ArchiveFactory +import org.readium.r2.shared.util.resource.Resource + +public class ZipArchiveFactory : ArchiveFactory { + + private val fileZipArchiveProvider = FileZipArchiveProvider() + + private val streamingZipArchiveProvider = StreamingZipArchiveProvider() + + override suspend fun create( + mediaType: MediaType, + blob: Blob, + password: String? + ): Try, ArchiveFactory.Error> = + blob.source?.toFile() + ?.let { fileZipArchiveProvider.create(mediaType, blob, password) } + ?: streamingZipArchiveProvider.create(mediaType, blob, password) +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt new file mode 100644 index 0000000000..593babd57b --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.zip + +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeHints +import org.readium.r2.shared.util.mediatype.MediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError + +public object ZipMediaTypeSniffer : MediaTypeSniffer { + + override fun sniffHints(hints: MediaTypeHints): Try { + if (hints.hasMediaType("application/zip") || + hints.hasFileExtension("zip") + ) { + return Try.success(MediaType.ZIP) + } + + return Try.failure(MediaTypeSnifferError.NotRecognized) + } + + override suspend fun sniffBlob(blob: Blob): Try { + blob.source?.toFile() + ?.let { return FileZipArchiveProvider().sniffBlob(blob) } + + return StreamingZipArchiveProvider().sniffBlob(blob) + } +} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt index af2dc6bec6..fe46327cd4 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt @@ -10,8 +10,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.archive.FileZipArchiveProvider import org.readium.r2.shared.util.resource.MediaTypeRetriever +import org.readium.r2.shared.util.zip.FileZipArchiveProvider import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt index 168e007be3..196eb63aee 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt @@ -6,8 +6,6 @@ import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.assertJSONEquals -import org.readium.r2.shared.util.archive.ArchiveProperties -import org.readium.r2.shared.util.archive.archive import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt index 3b140034c0..2eb5a51511 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -19,11 +19,11 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.archive.FileZipArchiveProvider import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.use +import org.readium.r2.shared.util.zip.FileZipArchiveProvider import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider import org.robolectric.ParameterizedRobolectricTestRunner diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt index 3fb4c54b8a..2135cfb045 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt @@ -17,10 +17,10 @@ import org.readium.r2.shared.publication.presentation.Presentation import org.readium.r2.shared.publication.presentation.presentation import org.readium.r2.shared.publication.services.PositionsService import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.use /** diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt index be53686d6d..7bc707a426 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt @@ -21,11 +21,11 @@ import org.readium.r2.shared.publication.presentation.Presentation 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.archive.ArchiveProperties -import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.ArchiveProperties import org.readium.r2.shared.util.resource.Container import org.readium.r2.shared.util.resource.ResourceTry +import org.readium.r2.shared.util.resource.archive import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index a834a59a32..b00b994484 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -19,12 +19,12 @@ import org.junit.runner.RunWith import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.firstWithRel import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.archive.FileZipArchiveProvider import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toUrl +import org.readium.r2.shared.util.zip.FileZipArchiveProvider import org.readium.r2.streamer.parseBlocking import org.readium.r2.streamer.parser.PublicationParser import org.robolectric.RobolectricTestRunner diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index 4170c32814..2dcf1cf8d1 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -42,7 +42,7 @@ sealed class PublicationError( when (error) { is AssetRetriever.Error.ReadError -> ReadError(error.cause) - is AssetRetriever.Error.ArchiveFormatNotSupported -> + is AssetRetriever.Error.FormatNotSupported -> UnsupportedArchiveFormat(error) is AssetRetriever.Error.SchemeNotSupported -> UnsupportedScheme(error) From 2df555577c383386aaf3171658f0d63dd0a364ac Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Thu, 23 Nov 2023 15:43:14 +0100 Subject: [PATCH 29/86] Introduce BlobMediaTypeRetriever --- .../readium/r2/lcp/LcpContentProtection.kt | 4 +- .../r2/lcp/license/model/components/Link.kt | 6 +- .../r2/shared/util/asset/AssetRetriever.kt | 7 +- .../shared/util/asset/FileResourceFactory.kt | 2 +- .../shared/util/mediatype/FormatRegistry.kt | 50 +++------ .../r2/shared/util/resource/ArchiveFactory.kt | 4 +- .../util/resource/BlobMediaTypeRetriever.kt | 102 ++++++++++++++++++ .../util/resource/BlobResourceAdapter.kt | 23 ++++ .../util/resource/MediaTypeRetriever.kt | 102 +++++++----------- .../shared/util/zip/FileZipArchiveProvider.kt | 7 +- .../r2/shared/util/zip/FileZipContainer.kt | 13 +-- .../util/zip/StreamingZipArchiveProvider.kt | 7 +- .../shared/util/zip/StreamingZipContainer.kt | 12 ++- .../r2/shared/util/zip/ZipArchiveFactory.kt | 14 ++- .../readium/r2/streamer/PublicationFactory.kt | 29 ++++- .../java/org/readium/r2/testapp/Readium.kt | 29 +++-- .../readium/r2/testapp/data/BookRepository.kt | 2 - .../org/readium/r2/testapp/data/model/Book.kt | 8 -- .../readium/r2/testapp/domain/Bookshelf.kt | 2 - .../r2/testapp/reader/ReaderRepository.kt | 3 +- 20 files changed, 270 insertions(+), 156 deletions(-) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobMediaTypeRetriever.kt diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index cbed453463..d77859add0 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -21,7 +21,6 @@ import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.TransformingContainer @@ -148,8 +147,7 @@ internal class LcpContentProtection( if (link.mediaType != null) { assetRetriever.retrieve( url, - mediaType = link.mediaType, - containerType = if (link.mediaType.isZip) MediaType.ZIP else null + mediaType = link.mediaType ) .map { it as Asset.Container } .mapFailure { it.wrap() } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt index aed3a404cc..c322e8d0f7 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt @@ -19,7 +19,6 @@ import org.readium.r2.shared.extensions.optStringsFromArrayOrSingle import org.readium.r2.shared.publication.Href import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.MediaTypeRetriever public data class Link( val href: Href, @@ -33,8 +32,7 @@ public data class Link( public companion object { public operator fun invoke( - json: JSONObject, - mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever() + json: JSONObject ): Link { val href = json.optNullableString("href") ?.let { @@ -48,7 +46,7 @@ public data class Link( return Link( href = href, mediaType = json.optNullableString("type") - ?.let { mediaTypeRetriever.retrieve(it) }, + ?.let { MediaType(it) }, title = json.optNullableString("title"), rels = json.optStringsFromArrayOrSingle("rel").toSet() .takeIf { it.isNotEmpty() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 4558b17ceb..6a2ef2f56f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -21,16 +21,15 @@ import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.SmartArchiveFactory import org.readium.r2.shared.util.resource.invoke import org.readium.r2.shared.util.toUrl -import org.readium.r2.shared.util.zip.ZipArchiveFactory /** * Retrieves an [Asset] instance providing reading access to the resource(s) of an asset stored at a * given [Url]. */ public class AssetRetriever( - private val resourceFactory: ResourceFactory = FileResourceFactory(), - archiveFactory: ArchiveFactory = ZipArchiveFactory(), - private val mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(), + private val mediaTypeRetriever: MediaTypeRetriever, + private val resourceFactory: ResourceFactory, + archiveFactory: ArchiveFactory, formatRegistry: FormatRegistry = FormatRegistry() ) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt index 0083093805..f0d5269fb3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt @@ -17,7 +17,7 @@ import org.readium.r2.shared.util.resource.filename import org.readium.r2.shared.util.resource.mediaType public class FileResourceFactory( - private val mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever() + private val mediaTypeRetriever: MediaTypeRetriever ) : ResourceFactory { override suspend fun create( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt index a369f27c01..7a753f9da4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt @@ -28,27 +28,33 @@ public class FormatRegistry( MediaType.ZAB to "zab" ), parentMediaTypes: Map = mapOf( + MediaType.CBZ to MediaType.ZIP, + MediaType.DIVINA to MediaType.READIUM_WEBPUB, + MediaType.DIVINA_MANIFEST to MediaType.READIUM_WEBPUB_MANIFEST, MediaType.EPUB to MediaType.ZIP, + MediaType.LCP_LICENSE_DOCUMENT to MediaType.JSON, + MediaType.LCP_PROTECTED_AUDIOBOOK to MediaType.READIUM_AUDIOBOOK, + MediaType.LCP_PROTECTED_PDF to MediaType.READIUM_WEBPUB, MediaType.READIUM_AUDIOBOOK to MediaType.READIUM_WEBPUB, - MediaType.READIUM_WEBPUB to MediaType.ZIP - ), - archiveMediaTypes: List = listOf(MediaType.ZIP) + MediaType.READIUM_AUDIOBOOK_MANIFEST to MediaType.READIUM_WEBPUB_MANIFEST, + MediaType.READIUM_WEBPUB to MediaType.ZIP, + MediaType.READIUM_WEBPUB_MANIFEST to MediaType.JSON, + MediaType.W3C_WPUB_MANIFEST to MediaType.JSON, + MediaType.ZAB to MediaType.ZIP + ) ) { private val fileExtensions: MutableMap = fileExtensions.toMutableMap() private val parentMediaTypes: MutableMap = parentMediaTypes.toMutableMap() - private val archiveMediaTypes = archiveMediaTypes.toMutableList() - /** * Registers a new [fileExtension] for the given [mediaType]. */ public fun register( mediaType: MediaType, fileExtension: String?, - isArchive: Boolean, - parent: MediaType? + parentMediaType: MediaType? ) { if (fileExtension == null) { fileExtensions.remove(mediaType) @@ -56,16 +62,10 @@ public class FormatRegistry( fileExtensions[mediaType] = fileExtension } - if (parent == null) { + if (parentMediaType == null) { parentMediaTypes.remove(mediaType) } else { - parentMediaTypes[mediaType] = parent - } - - if (isArchive) { - archiveMediaTypes.add(mediaType) - } else { - archiveMediaTypes.remove(mediaType) + parentMediaTypes[mediaType] = parentMediaType } } @@ -77,24 +77,4 @@ public class FormatRegistry( public fun parentMediaType(mediaType: MediaType): MediaType? = parentMediaTypes[mediaType] - - public fun MediaType.isAlso(mediaType: MediaType): Boolean { - if (this == mediaType) { - return true - } - - return parentMediaTypes[this] - ?.isAlso(mediaType) - ?: false - } - - public val MediaType.isArchive: Boolean get() { - if (this in archiveMediaTypes) { - return true - } - - return parentMediaTypes[this] - ?.isArchive - ?: false - } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt index 98db5e5c00..09c963a78b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt @@ -53,7 +53,9 @@ public class CompositeArchiveFactory( private val factories: List ) : ArchiveFactory { - public constructor(vararg factories: ArchiveFactory) : this(factories.toList()) + public constructor(vararg factories: ArchiveFactory) : + this(factories.toList()) + override suspend fun create( mediaType: MediaType, blob: Blob, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobMediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobMediaTypeRetriever.kt new file mode 100644 index 0000000000..7058e78a25 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobMediaTypeRetriever.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.resource + +import android.content.ContentResolver +import android.provider.MediaStore +import java.io.File +import org.readium.r2.shared.DelicateReadiumApi +import org.readium.r2.shared.extensions.queryProjection +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeHints +import org.readium.r2.shared.util.mediatype.MediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.mediatype.SystemMediaTypeSniffer +import org.readium.r2.shared.util.toUri + +@DelicateReadiumApi +public class BlobMediaTypeRetriever( + private val mediaTypeSniffer: MediaTypeSniffer, + private val contentResolver: ContentResolver? +) { + + /** + * Retrieves a canonical [MediaType] for the provided media type and file extension [hints]. + */ + public fun retrieve(hints: MediaTypeHints): MediaType? { + mediaTypeSniffer.sniffHints(hints) + .getOrNull() + ?.let { return it } + + // Falls back on the system-wide registered media types using MimeTypeMap. + // Note: This is done after the default sniffers, because otherwise it will detect + // JSON, XML or ZIP formats before we have a chance of sniffing their content (for example, + // for RWPM). + SystemMediaTypeSniffer.sniffHints(hints) + .getOrNull() + ?.let { return it } + + return hints.mediaTypes.firstOrNull() + } + + public suspend fun retrieve(hints: MediaTypeHints, blob: Blob): Try { + mediaTypeSniffer.sniffBlob(blob) + .onSuccess { return Try.success(it) } + .onFailure { error -> + when (error) { + is MediaTypeSnifferError.NotRecognized -> {} + else -> return Try.failure(error) + } + } + + // Falls back on the system-wide registered media types using MimeTypeMap. + // Note: This is done after the default sniffers, because otherwise it will detect + // JSON, XML or ZIP formats before we have a chance of sniffing their content (for example, + // for RWPM). + SystemMediaTypeSniffer.sniffHints(hints) + .getOrNull() + ?.let { return Try.success(it) } + + SystemMediaTypeSniffer.sniffBlob(blob) + .onSuccess { return Try.success(it) } + .onFailure { error -> + when (error) { + is MediaTypeSnifferError.NotRecognized -> {} + else -> return Try.failure(error) + } + } + + // Falls back on the [contentResolver] in case of content Uri. + // Note: This is done after the heavy sniffing of the provided [sniffers], because + // otherwise it will detect JSON, XML or ZIP formats before we have a chance of sniffing + // their content (for example, for RWPM). + + if (contentResolver != null) { + blob.source + ?.takeIf { it.isContent } + ?.let { url -> + val contentHints = MediaTypeHints( + mediaType = contentResolver.getType(url.toUri()) + ?.let { MediaType(it) } + ?.takeUnless { it.matches(MediaType.BINARY) }, + fileExtension = contentResolver + .queryProjection(url.uri, MediaStore.MediaColumns.DISPLAY_NAME) + ?.let { filename -> File(filename).extension } + ) + + retrieve(contentHints) + ?.let { return Try.success(it) } + } + } + + return hints.mediaTypes.firstOrNull() + ?.let { Try.success(it) } + ?: Try.failure(MediaTypeSnifferError.NotRecognized) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt index 63a00cd3d5..4dd585b2ad 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt @@ -7,7 +7,9 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints @@ -41,3 +43,24 @@ internal class BlobResourceAdapter( Resource.Properties(properties) ) } + +internal class BlobContainerAdapter( + private val container: Container, + private val properties: Map, + private val mediaTypeRetriever: MediaTypeRetriever +) : Container { + override val entries: Set = + container.entries + + override fun get(url: Url): Resource? { + val blob = container[url] ?: return null + + val resourceProperties = properties[url] ?: Resource.Properties() + + return BlobResourceAdapter(blob, resourceProperties, mediaTypeRetriever) + } + + override suspend fun close() { + container.close() + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt index 0c1f931d5c..f6a2a10652 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt @@ -7,9 +7,8 @@ package org.readium.r2.shared.util.resource import android.content.ContentResolver -import android.provider.MediaStore import java.io.File -import org.readium.r2.shared.extensions.queryProjection +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container @@ -22,7 +21,6 @@ import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeSniffer import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.mediatype.SystemMediaTypeSniffer -import org.readium.r2.shared.util.toUri import org.readium.r2.shared.util.zip.ZipArchiveFactory /** @@ -32,12 +30,40 @@ import org.readium.r2.shared.util.zip.ZipArchiveFactory * The actual format sniffing is mostly done by the provided [mediaTypeSniffer]. * The [DefaultMediaTypeSniffer] cover the formats supported with Readium by default. */ +@OptIn(DelicateReadiumApi::class) public class MediaTypeRetriever( - private val contentResolver: ContentResolver? = null, - archiveFactory: ArchiveFactory = ZipArchiveFactory(), - private val mediaTypeSniffer: MediaTypeSniffer = DefaultMediaTypeSniffer(), - formatRegistry: FormatRegistry = FormatRegistry() -) : MediaTypeSniffer { + private val mediaTypeSniffer: MediaTypeSniffer, + formatRegistry: FormatRegistry, + archiveFactory: ArchiveFactory, + contentResolver: ContentResolver? +) { + + public companion object { + + @Deprecated("This overload will be removed without notice as soon as possible.") + public operator fun invoke( + contentResolver: ContentResolver? = null + ): MediaTypeRetriever { + val mediaTypeSniffer = + DefaultMediaTypeSniffer() + + val archiveFactory = + ZipArchiveFactory(mediaTypeSniffer) + + val formatRegistry = + FormatRegistry() + + return MediaTypeRetriever( + mediaTypeSniffer, + formatRegistry, + archiveFactory, + contentResolver + ) + } + } + + private val blobMediaTypeRetriever: BlobMediaTypeRetriever = + BlobMediaTypeRetriever(mediaTypeSniffer, contentResolver) private val archiveFactory: ArchiveFactory = SmartArchiveFactory(archiveFactory, formatRegistry) @@ -46,8 +72,7 @@ public class MediaTypeRetriever( * Retrieves a canonical [MediaType] for the provided media type and file extension [hints]. */ public fun retrieve(hints: MediaTypeHints): MediaType? { - mediaTypeSniffer.sniffHints(hints) - .getOrNull() + blobMediaTypeRetriever.retrieve(hints) ?.let { return it } // Falls back on the system-wide registered media types using MimeTypeMap. @@ -127,7 +152,7 @@ public class MediaTypeRetriever( .getOrNull() ?.let { return Try.success(it) } - val blobMediaType = doSniffBlob(hints, blob) + val blobMediaType = blobMediaTypeRetriever.retrieve(hints, blob) .getOrElse { return Try.failure(it) } val container = archiveFactory.create(blobMediaType, blob) @@ -142,59 +167,4 @@ public class MediaTypeRetriever( return retrieve(hints, container) } - - private suspend fun doSniffBlob(hints: MediaTypeHints, blob: Blob): Try { - mediaTypeSniffer.sniffBlob(blob) - .onSuccess { return Try.success(it) } - .onFailure { error -> - when (error) { - is MediaTypeSnifferError.NotRecognized -> {} - else -> return Try.failure(error) - } - } - - // Falls back on the system-wide registered media types using MimeTypeMap. - // Note: This is done after the default sniffers, because otherwise it will detect - // JSON, XML or ZIP formats before we have a chance of sniffing their content (for example, - // for RWPM). - SystemMediaTypeSniffer.sniffHints(hints) - .getOrNull() - ?.let { return Try.success(it) } - - SystemMediaTypeSniffer.sniffBlob(blob) - .onSuccess { return Try.success(it) } - .onFailure { error -> - when (error) { - is MediaTypeSnifferError.NotRecognized -> {} - else -> return Try.failure(error) - } - } - - // Falls back on the [contentResolver] in case of content Uri. - // Note: This is done after the heavy sniffing of the provided [sniffers], because - // otherwise it will detect JSON, XML or ZIP formats before we have a chance of sniffing - // their content (for example, for RWPM). - - if (contentResolver != null) { - blob.source - ?.takeIf { it.isContent } - ?.let { url -> - val contentHints = MediaTypeHints( - mediaType = contentResolver.getType(url.toUri()) - ?.let { MediaType(it) } - ?.takeUnless { it.matches(MediaType.BINARY) }, - fileExtension = contentResolver - .queryProjection(url.uri, MediaStore.MediaColumns.DISPLAY_NAME) - ?.let { filename -> File(filename).extension } - ) - - retrieve(contentHints) - ?.let { return Try.success(it) } - } - } - - return hints.mediaTypes.firstOrNull() - ?.let { Try.success(it) } - ?: Try.failure(MediaTypeSnifferError.NotRecognized) - } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index 67eb2b7a0f..460edf0ef2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -13,6 +13,7 @@ import java.util.zip.ZipException import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob @@ -23,12 +24,16 @@ import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.ArchiveFactory +import org.readium.r2.shared.util.resource.BlobMediaTypeRetriever import org.readium.r2.shared.util.resource.Resource /** * An [ArchiveFactory] to open local ZIP files with Java's [ZipFile]. */ -internal class FileZipArchiveProvider { +@OptIn(DelicateReadiumApi::class) +internal class FileZipArchiveProvider( + private val mediaTypeRetriever: BlobMediaTypeRetriever? = null +) { suspend fun sniffBlob(blob: Blob): Try { val file = blob.source?.toFile() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt index 6b7c83d1b1..e643d2afbf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt @@ -13,6 +13,7 @@ import java.util.zip.ZipException import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.extensions.readFully import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl @@ -29,16 +30,16 @@ import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.ArchiveProperties -import org.readium.r2.shared.util.resource.MediaTypeRetriever +import org.readium.r2.shared.util.resource.BlobMediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.tryRecover - +@OptIn(DelicateReadiumApi::class) internal class FileZipContainer( private val archive: ZipFile, file: File, - private val mediaTypeRetriever: MediaTypeRetriever + private val mediaTypeRetriever: BlobMediaTypeRetriever? ) : Container { private inner class Entry(private val url: Url, private val entry: ZipEntry) : @@ -47,17 +48,17 @@ internal class FileZipContainer( override val source: AbsoluteUrl? = null override suspend fun mediaType(): Try = - mediaTypeRetriever.retrieve( + mediaTypeRetriever?.retrieve( hints = MediaTypeHints(fileExtension = url.extension), blob = this - ).tryRecover { error -> + )?.tryRecover { error -> when (error) { is MediaTypeSnifferError.Read -> Try.failure(error.cause) MediaTypeSnifferError.NotRecognized -> Try.success(MediaType.BINARY) } - } + } ?: Try.success(MediaType.BINARY) override suspend fun properties(): Try = Try.success( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 8bf3886b4c..85c2f69f94 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -10,6 +10,7 @@ import java.io.File import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.extensions.unwrapInstance import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError @@ -21,6 +22,7 @@ import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.ArchiveFactory +import org.readium.r2.shared.util.resource.BlobMediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toUrl @@ -31,7 +33,10 @@ import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel * An [ArchiveFactory] able to open a ZIP archive served through a stream (e.g. HTTP server, * content URI, etc.). */ -internal class StreamingZipArchiveProvider { +@OptIn(DelicateReadiumApi::class) +internal class StreamingZipArchiveProvider( + private val mediaTypeRetriever: BlobMediaTypeRetriever? = null +) { suspend fun sniffBlob(blob: Blob): Try { return try { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt index 91685dbcfa..5191fc1be7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt @@ -8,6 +8,7 @@ package org.readium.r2.shared.util.zip import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.extensions.readFully import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.extensions.unwrapInstance @@ -25,7 +26,7 @@ import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.ArchiveProperties -import org.readium.r2.shared.util.resource.MediaTypeRetriever +import org.readium.r2.shared.util.resource.BlobMediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.archive @@ -33,10 +34,11 @@ import org.readium.r2.shared.util.tryRecover import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipArchiveEntry import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile +@OptIn(DelicateReadiumApi::class) internal class StreamingZipContainer( private val zipFile: ZipFile, override val source: AbsoluteUrl?, - private val mediaTypeRetriever: MediaTypeRetriever + private val mediaTypeRetriever: BlobMediaTypeRetriever? ) : Container { private inner class Entry( @@ -58,17 +60,17 @@ internal class StreamingZipContainer( ) override suspend fun mediaType(): ResourceTry = - mediaTypeRetriever.retrieve( + mediaTypeRetriever?.retrieve( hints = MediaTypeHints(fileExtension = url.extension), blob = this - ).tryRecover { error -> + )?.tryRecover { error -> when (error) { is MediaTypeSnifferError.Read -> Try.failure(error.cause) MediaTypeSnifferError.NotRecognized -> Try.success(MediaType.BINARY) } - } + } ?: Try.success(MediaType.BINARY) override suspend fun length(): ResourceTry = entry.size.takeUnless { it == -1L } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt index 503adaace8..fac44d2600 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt @@ -6,18 +6,26 @@ package org.readium.r2.shared.util.zip +import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeSniffer import org.readium.r2.shared.util.resource.ArchiveFactory +import org.readium.r2.shared.util.resource.BlobMediaTypeRetriever import org.readium.r2.shared.util.resource.Resource -public class ZipArchiveFactory : ArchiveFactory { +@OptIn(DelicateReadiumApi::class) +public class ZipArchiveFactory( + mediaTypeSniffer: MediaTypeSniffer +) : ArchiveFactory { - private val fileZipArchiveProvider = FileZipArchiveProvider() + private val mediaTypeRetriever = BlobMediaTypeRetriever(mediaTypeSniffer, null) - private val streamingZipArchiveProvider = StreamingZipArchiveProvider() + private val fileZipArchiveProvider = FileZipArchiveProvider(mediaTypeRetriever) + + private val streamingZipArchiveProvider = StreamingZipArchiveProvider(mediaTypeRetriever) override suspend fun create( mediaType: MediaType, diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 4ceb83e67a..b3069c5c4a 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -6,6 +6,7 @@ package org.readium.r2.streamer +import android.content.ContentResolver import android.content.Context import org.readium.r2.shared.PdfSupport import org.readium.r2.shared.publication.Publication @@ -19,9 +20,11 @@ import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.logging.WarningLogger +import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.pdf.PdfDocumentFactory import org.readium.r2.shared.util.resource.MediaTypeRetriever +import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.readium.r2.streamer.parser.PublicationParser import org.readium.r2.streamer.parser.audio.AudioParser import org.readium.r2.streamer.parser.epub.EpubParser @@ -80,14 +83,32 @@ public class PublicationFactory( public operator fun invoke( context: Context, contentProtections: List = emptyList(), - onCreatePublication: Publication.Builder.() -> Unit + onCreatePublication: Publication.Builder.() -> Unit, + contentResolver: ContentResolver? = null ): PublicationFactory { + val mediaTypeSniffer = + DefaultMediaTypeSniffer() + + val archiveFactory = + ZipArchiveFactory(mediaTypeSniffer) + + val formatRegistry = + FormatRegistry() + + val mediaTypeRetriever = + MediaTypeRetriever( + mediaTypeSniffer, + FormatRegistry(), + archiveFactory, + contentResolver + ) + return PublicationFactory( context = context, contentProtections = contentProtections, - formatRegistry = FormatRegistry(), - mediaTypeRetriever = MediaTypeRetriever(), - httpClient = DefaultHttpClient(MediaTypeRetriever()), + formatRegistry = formatRegistry, + mediaTypeRetriever = mediaTypeRetriever, + httpClient = DefaultHttpClient(mediaTypeRetriever), pdfFactory = null, onCreatePublication = onCreatePublication ) diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 28f797bead..3cbf6869aa 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -24,9 +24,10 @@ import org.readium.r2.shared.util.asset.FileResourceFactory import org.readium.r2.shared.util.asset.HttpResourceFactory import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager import org.readium.r2.shared.util.http.DefaultHttpClient +import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.resource.MediaTypeRetriever -import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider +import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.readium.r2.streamer.PublicationFactory /** @@ -34,17 +35,27 @@ import org.readium.r2.streamer.PublicationFactory */ class Readium(context: Context) { - private val mediaTypeRetriever = MediaTypeRetriever() + private val mediaTypeSniffer = + DefaultMediaTypeSniffer() - val formatRegistry = FormatRegistry() + private val archiveFactory = + ZipArchiveFactory(mediaTypeSniffer) + + val formatRegistry = + FormatRegistry() + + private val mediaTypeRetriever = + MediaTypeRetriever( + mediaTypeSniffer, + formatRegistry, + archiveFactory, + context.contentResolver + ) val httpClient = DefaultHttpClient( - mediaTypeRetriever = mediaTypeRetriever + mediaTypeRetriever ) - private val archiveProvider = - StreamingZipArchiveProvider(mediaTypeRetriever) - private val resourceFactory = CompositeResourceFactory( FileResourceFactory(mediaTypeRetriever), ContentResourceFactory(context.contentResolver, mediaTypeRetriever), @@ -52,8 +63,10 @@ class Readium(context: Context) { ) val assetRetriever = AssetRetriever( + mediaTypeRetriever, resourceFactory, - archiveProvider + archiveFactory, + formatRegistry ) val downloadManager = AndroidDownloadManager( diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt index 218ba91809..ad8a8fa916 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt @@ -82,7 +82,6 @@ class BookRepository( suspend fun insertBook( url: Url, mediaType: MediaType, - containerType: MediaType?, drm: ContentProtection.Scheme?, publication: Publication, cover: File @@ -94,7 +93,6 @@ class BookRepository( href = url.toString(), identifier = publication.metadata.identifier ?: "", mediaType = mediaType, - containerType = containerType, drm = drm, progression = "{}", cover = cover.path diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt b/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt index f7925ce05d..14aca599ac 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt @@ -32,8 +32,6 @@ data class Book( val progression: String? = null, @ColumnInfo(name = MEDIA_TYPE) val rawMediaType: String, - @ColumnInfo(name = CONTAINER_TYPE) - val rawContainerType: String, @ColumnInfo(name = DRM) val drm: String? = null, @ColumnInfo(name = COVER) @@ -49,7 +47,6 @@ data class Book( identifier: String, progression: String? = null, mediaType: MediaType, - containerType: MediaType?, drm: ContentProtection.Scheme?, cover: String ) : this( @@ -61,7 +58,6 @@ data class Book( identifier = identifier, progression = progression, rawMediaType = mediaType.toString(), - rawContainerType = containerType.toString(), drm = drm?.uri, cover = cover ) @@ -74,9 +70,6 @@ data class Book( val drmScheme: ContentProtection.Scheme? get() = drm?.let { ContentProtection.Scheme(it) } - val containerType: MediaType? get() = - MediaType(rawContainerType) - companion object { const val TABLE_NAME = "books" @@ -88,7 +81,6 @@ data class Book( const val IDENTIFIER = "identifier" const val PROGRESSION = "progression" const val MEDIA_TYPE = "media_type" - const val CONTAINER_TYPE = "container_type" const val COVER = "cover" const val DRM = "drm" } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index 41ec250a08..fb2c1531ce 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -17,7 +17,6 @@ import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetri import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError @@ -167,7 +166,6 @@ class Bookshelf( val id = bookRepository.insertBook( url, asset.mediaType, - (asset as? Asset.Container)?.containerType, drmScheme, publication, coverFile diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt index ab98cc0454..74989df362 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt @@ -74,8 +74,7 @@ class ReaderRepository( val asset = readium.assetRetriever.retrieve( book.url, - book.mediaType, - book.containerType + book.mediaType ).getOrElse { return Try.failure( OpeningError.PublicationError( From d92f49df31134ccb747c7a84bbd38c278d848439 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 27 Nov 2023 10:23:43 +0100 Subject: [PATCH 30/86] Remove passwords from ArchiveFactory --- .../r2/shared/util/asset/AssetRetriever.kt | 2 -- .../r2/shared/util/http/DefaultHttpClient.kt | 2 +- .../r2/shared/util/resource/ArchiveFactory.kt | 16 +++------------- .../shared/util/resource/SmartArchiveFactory.kt | 9 +++------ .../r2/shared/util/zip/FileZipArchiveProvider.kt | 7 +------ .../util/zip/StreamingZipArchiveProvider.kt | 7 +------ .../r2/shared/util/zip/ZipArchiveFactory.kt | 7 +++---- 7 files changed, 12 insertions(+), 38 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 6a2ef2f56f..d4c0bd55c1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -70,8 +70,6 @@ public class AssetRetriever( Try.failure(Error.ReadError(it.cause)) is ArchiveFactory.Error.FormatNotSupported -> Try.success(Asset.Resource(mediaType, resource)) - is ArchiveFactory.Error.PasswordsNotSupported -> - Try.failure(Error.FormatNotSupported(it)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index 4562aad694..da3e961918 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -55,7 +55,7 @@ public class DefaultHttpClient( public var callback: Callback = object : Callback {} ) : HttpClient { - @Suppress("UNUSED_PARAMETER") + @Suppress("UNUSED_PARAMETER", "DEPRECATION") @Deprecated( "You need to provide a [mediaTypeRetriever]. If you used [additionalHeaders], pass all headers when building your request or modify it in Callback.onStartRequest instead.", level = DeprecationLevel.ERROR, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt index 09c963a78b..e1358950f5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt @@ -6,7 +6,6 @@ package org.readium.r2.shared.util.resource -import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container @@ -23,13 +22,6 @@ public interface ArchiveFactory { override val cause: org.readium.r2.shared.util.Error? ) : org.readium.r2.shared.util.Error { - public class PasswordsNotSupported( - cause: org.readium.r2.shared.util.Error? = null - ) : Error("Password feature is not supported.", cause) { - - public constructor(exception: Exception) : this(ThrowableError(exception)) - } - public class FormatNotSupported( cause: org.readium.r2.shared.util.Error? = null ) : Error("Resource is not supported.", cause) @@ -44,8 +36,7 @@ public interface ArchiveFactory { */ public suspend fun create( mediaType: MediaType, - blob: Blob, - password: String? = null + blob: Blob ): Try, Error> } @@ -58,11 +49,10 @@ public class CompositeArchiveFactory( override suspend fun create( mediaType: MediaType, - blob: Blob, - password: String? + blob: Blob ): Try, ArchiveFactory.Error> { for (factory in factories) { - factory.create(mediaType, blob, password) + factory.create(mediaType, blob) .getOrElse { error -> when (error) { is ArchiveFactory.Error.FormatNotSupported -> null diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt index b7adbcfb15..9d4d745195 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt @@ -20,19 +20,16 @@ internal class SmartArchiveFactory( override suspend fun create( mediaType: MediaType, - blob: Blob, - password: String? + blob: Blob ): Try, ArchiveFactory.Error> = - archiveFactory.create(mediaType, blob, password) + archiveFactory.create(mediaType, blob) .tryRecover { error -> when (error) { is ArchiveFactory.Error.FormatNotSupported -> { formatRegistry.parentMediaType(mediaType) - ?.let { archiveFactory.create(it, blob, password) } + ?.let { archiveFactory.create(it, blob) } ?: Try.failure(error) } - is ArchiveFactory.Error.PasswordsNotSupported -> - Try.failure(error) is ArchiveFactory.Error.ReadError -> Try.failure(error) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index 460edf0ef2..f403bd7c9d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -63,8 +63,7 @@ internal class FileZipArchiveProvider( suspend fun create( mediaType: MediaType, - blob: Blob, - password: String? + blob: Blob ): Try, ArchiveFactory.Error> { if (mediaType != MediaType.ZIP) { return Try.failure( @@ -74,10 +73,6 @@ internal class FileZipArchiveProvider( ) } - if (password != null) { - return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) - } - val file = blob.source?.toFile() ?: return Try.Failure( ArchiveFactory.Error.FormatNotSupported( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 85c2f69f94..9d4a36f404 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -54,8 +54,7 @@ internal class StreamingZipArchiveProvider( suspend fun create( mediaType: MediaType, - blob: Blob, - password: String? + blob: Blob ): Try, ArchiveFactory.Error> { if (mediaType != MediaType.ZIP) { return Try.failure( @@ -65,10 +64,6 @@ internal class StreamingZipArchiveProvider( ) } - if (password != null) { - return Try.failure(ArchiveFactory.Error.PasswordsNotSupported()) - } - return try { val container = openBlob( blob, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt index fac44d2600..7ba29253dc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt @@ -29,10 +29,9 @@ public class ZipArchiveFactory( override suspend fun create( mediaType: MediaType, - blob: Blob, - password: String? + blob: Blob ): Try, ArchiveFactory.Error> = blob.source?.toFile() - ?.let { fileZipArchiveProvider.create(mediaType, blob, password) } - ?: streamingZipArchiveProvider.create(mediaType, blob, password) + ?.let { fileZipArchiveProvider.create(mediaType, blob) } + ?: streamingZipArchiveProvider.create(mediaType, blob) } From 6b3688c9664b6b5c58b67a69a35bc001676f6981 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 27 Nov 2023 11:22:00 +0100 Subject: [PATCH 31/86] Cosmetic change --- .../org/readium/r2/shared/util/asset/Asset.kt | 1 - .../r2/shared/util/asset/AssetRetriever.kt | 4 ++-- .../r2/shared/util/mediatype/FormatRegistry.kt | 16 ++++++++-------- .../shared/util/resource/SmartArchiveFactory.kt | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt index bec71340e3..a05e89c62e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt @@ -43,7 +43,6 @@ public sealed class Asset { * A container asset providing access to several resources. * * @param mediaType Media type of the asset. - * @param containerType Media type of the container. * @param container Opened container to access asset resources. */ public class Container( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index d4c0bd55c1..44ad50ea3f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -44,7 +44,7 @@ public class AssetRetriever( ) : Error("Scheme $scheme is not supported.", cause) public class FormatNotSupported(cause: org.readium.r2.shared.util.Error) : - Error("Archive providers do not support this kind of archive.", cause) + Error("Asset format is not supported.", cause) public class ReadError(override val cause: org.readium.r2.shared.util.data.ReadError) : Error("An error occurred when trying to read asset.", cause) @@ -54,7 +54,7 @@ public class AssetRetriever( SmartArchiveFactory(archiveFactory, formatRegistry) /** - * Retrieves an asset from a known media and asset type. + * Retrieves an asset from a known media type. */ public suspend fun retrieve( url: AbsoluteUrl, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt index 7a753f9da4..7da8aa8494 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt @@ -27,7 +27,7 @@ public class FormatRegistry( MediaType.W3C_WPUB_MANIFEST to "json", MediaType.ZAB to "zab" ), - parentMediaTypes: Map = mapOf( + superTypes: Map = mapOf( MediaType.CBZ to MediaType.ZIP, MediaType.DIVINA to MediaType.READIUM_WEBPUB, MediaType.DIVINA_MANIFEST to MediaType.READIUM_WEBPUB_MANIFEST, @@ -46,7 +46,7 @@ public class FormatRegistry( private val fileExtensions: MutableMap = fileExtensions.toMutableMap() - private val parentMediaTypes: MutableMap = parentMediaTypes.toMutableMap() + private val superTypes: MutableMap = superTypes.toMutableMap() /** * Registers a new [fileExtension] for the given [mediaType]. @@ -54,7 +54,7 @@ public class FormatRegistry( public fun register( mediaType: MediaType, fileExtension: String?, - parentMediaType: MediaType? + superType: MediaType? ) { if (fileExtension == null) { fileExtensions.remove(mediaType) @@ -62,10 +62,10 @@ public class FormatRegistry( fileExtensions[mediaType] = fileExtension } - if (parentMediaType == null) { - parentMediaTypes.remove(mediaType) + if (superType == null) { + superTypes.remove(mediaType) } else { - parentMediaTypes[mediaType] = parentMediaType + superTypes[mediaType] = superType } } @@ -75,6 +75,6 @@ public class FormatRegistry( public fun fileExtension(mediaType: MediaType): String? = fileExtensions[mediaType] - public fun parentMediaType(mediaType: MediaType): MediaType? = - parentMediaTypes[mediaType] + public fun superType(mediaType: MediaType): MediaType? = + superTypes[mediaType] } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt index 9d4d745195..03df02a9f3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt @@ -26,7 +26,7 @@ internal class SmartArchiveFactory( .tryRecover { error -> when (error) { is ArchiveFactory.Error.FormatNotSupported -> { - formatRegistry.parentMediaType(mediaType) + formatRegistry.superType(mediaType) ?.let { archiveFactory.create(it, blob) } ?: Try.failure(error) } From 58a9846ef082988ba2377cabcb70411ea168a443 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 27 Nov 2023 13:11:24 +0100 Subject: [PATCH 32/86] Remove Resource.mediaType() --- .../readium/r2/lcp/LcpContentProtection.kt | 6 +- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 58 ++------------- .../readium/r2/lcp/service/LicensesService.kt | 2 +- .../readium/r2/navigator/epub/HtmlInjector.kt | 12 ++-- .../r2/navigator/epub/WebViewServer.kt | 28 ++++---- .../r2/shared/publication/Publication.kt | 16 +---- .../iterators/HtmlResourceContentIterator.kt | 3 +- .../iterators/PublicationContentIterator.kt | 7 +- .../services/search/StringSearchService.kt | 5 +- .../r2/shared/util/asset/AssetRetriever.kt | 23 +++--- .../util/asset/ContentResourceFactory.kt | 34 ++------- .../shared/util/asset/FileResourceFactory.kt | 28 +------- .../shared/util/asset/HttpResourceFactory.kt | 6 +- .../readium/r2/shared/util/data/Container.kt | 6 +- .../readium/r2/shared/util/data/Decoding.kt | 12 ++-- .../shared/util/data/{Blob.kt => Readable.kt} | 8 +-- ...bInputStream.kt => ReadableInputStream.kt} | 12 ++-- .../android/AndroidDownloadManager.kt | 13 ++-- .../r2/shared/util/http/DefaultHttpClient.kt | 23 +++++- .../r2/shared/util/http/HttpContainer.kt | 6 +- .../r2/shared/util/http/HttpResource.kt | 28 +------- .../util/mediatype/DefaultMediaTypeSniffer.kt | 6 +- .../shared/util/mediatype/FormatRegistry.kt | 3 + .../shared/util/mediatype/MediaTypeSniffer.kt | 72 +++++++++---------- .../r2/shared/util/resource/ArchiveFactory.kt | 10 +-- .../util/resource/BlobMediaTypeRetriever.kt | 10 +-- .../util/resource/BlobResourceAdapter.kt | 66 ----------------- .../ContentResource.kt} | 31 ++++++-- .../util/resource/DirectoryContainer.kt | 19 +---- .../shared/util/resource/FallbackResource.kt | 4 -- .../FileBlob.kt => resource/FileResource.kt} | 27 +++++-- .../InMemoryResource.kt} | 19 +++-- .../r2/shared/util/resource/LazyResource.kt | 4 -- .../util/resource/MediaTypeRetriever.kt | 40 ++++++----- .../r2/shared/util/resource/Resource.kt | 21 ++---- .../util/resource/SmartArchiveFactory.kt | 8 +-- .../r2/shared/util/resource/StringResource.kt | 19 ++--- .../util/resource/SynchronizedResource.kt | 4 -- .../content/ResourceContentExtractor.kt | 6 +- .../shared/util/zip/FileZipArchiveProvider.kt | 26 ++----- .../r2/shared/util/zip/FileZipContainer.kt | 24 +------ .../{BlobChannel.kt => ReadableChannel.kt} | 14 ++-- .../util/zip/StreamingZipArchiveProvider.kt | 27 +++---- .../shared/util/zip/StreamingZipContainer.kt | 23 +----- .../r2/shared/util/zip/ZipArchiveFactory.kt | 24 +++---- .../r2/shared/util/zip/ZipMediaTypeSniffer.kt | 11 +-- .../publication/services/CoverServiceTest.kt | 4 +- .../util/resource/BufferingResourceTest.kt | 3 +- .../util/resource/ResourceInputStreamTest.kt | 3 +- .../shared/util/resource/ZipContainerTest.kt | 3 +- .../readium/r2/streamer/ParserAssetFactory.kt | 6 +- .../readium/r2/streamer/PublicationFactory.kt | 12 ++-- .../readium/r2/streamer/extensions/Link.kt | 29 -------- .../r2/streamer/parser/audio/AudioParser.kt | 28 ++++++-- .../r2/streamer/parser/image/ImageParser.kt | 43 ++++++++--- .../r2/streamer/parser/pdf/PdfParser.kt | 3 +- .../streamer/parser/image/ImageParserTest.kt | 6 +- .../java/org/readium/r2/testapp/Readium.kt | 8 +-- 58 files changed, 375 insertions(+), 627 deletions(-) rename readium/shared/src/main/java/org/readium/r2/shared/util/data/{Blob.kt => Readable.kt} (84%) rename readium/shared/src/main/java/org/readium/r2/shared/util/data/{BlobInputStream.kt => ReadableInputStream.kt} (92%) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/{data/ContentBlob.kt => resource/ContentResource.kt} (80%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{data/FileBlob.kt => resource/FileResource.kt} (82%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{data/InMemoryBlob.kt => resource/InMemoryResource.kt} (72%) rename readium/shared/src/main/java/org/readium/r2/shared/util/zip/{BlobChannel.kt => ReadableChannel.kt} (88%) delete mode 100644 readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index d77859add0..3c78dca446 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -21,14 +21,12 @@ import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.TransformingContainer internal class LcpContentProtection( private val lcpService: LcpService, private val authentication: LcpAuthenticating, - private val assetRetriever: AssetRetriever, - private val mediaTypeRetriever: MediaTypeRetriever + private val assetRetriever: AssetRetriever ) : ContentProtection { override val scheme: ContentProtection.Scheme = @@ -78,7 +76,7 @@ internal class LcpContentProtection( val serviceFactory = LcpContentProtectionService .createFactory(license.getOrNull(), license.failureOrNull()) - val decryptor = LcpDecryptor(license.getOrNull(), mediaTypeRetriever) + val decryptor = LcpDecryptor(license.getOrNull()) val container = TransformingContainer(asset.container, decryptor::transform) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 57f17e90c1..d4fd40e88b 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -20,22 +20,16 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.FailureResource -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.TransformingResource import org.readium.r2.shared.util.resource.flatMap -import org.readium.r2.shared.util.tryRecover /** * Decrypts a resource protected with LCP. */ internal class LcpDecryptor( val license: LcpLicense?, - private val mediaTypeRetriever: MediaTypeRetriever, var encryptionData: Map = emptyMap() ) { @@ -59,21 +53,9 @@ internal class LcpDecryptor( ) ) encryption.isDeflated || !encryption.isCbcEncrypted -> - FullLcpResource( - url, - resource, - encryption, - license, - mediaTypeRetriever - ) + FullLcpResource(resource, encryption, license) else -> - CbcLcpResource( - url, - resource, - encryption, - license, - mediaTypeRetriever - ) + CbcLcpResource(resource, encryption, license) } } } @@ -85,29 +67,13 @@ internal class LcpDecryptor( * resource, for example when the resource is deflated before encryption. */ private class FullLcpResource( - private val url: Url, resource: Resource, private val encryption: Encryption, - private val license: LcpLicense, - private val mediaTypeRetriever: MediaTypeRetriever + private val license: LcpLicense ) : TransformingResource(resource) { override val source: AbsoluteUrl? = null - override suspend fun mediaType(): Try = - mediaTypeRetriever - .retrieve( - hints = MediaTypeHints(fileExtension = url.extension), - blob = this - ) - .tryRecover { error -> - when (error) { - is MediaTypeSnifferError.Read -> - Try.failure(error.cause) - MediaTypeSnifferError.NotRecognized -> - Try.success(MediaType.BINARY) - } - } override suspend fun transform(data: Try): Try = license.decryptFully(data, encryption.isDeflated) @@ -123,11 +89,9 @@ internal class LcpDecryptor( * Supports random access for byte range requests, but the resource MUST NOT be deflated. */ private class CbcLcpResource( - private val url: Url, private val resource: Resource, private val encryption: Encryption, - private val license: LcpLicense, - private val mediaTypeRetriever: MediaTypeRetriever + private val license: LcpLicense ) : Resource by resource { override val source: AbsoluteUrl? = null @@ -150,20 +114,6 @@ internal class LcpDecryptor( */ private val _cache: Cache = Cache() - override suspend fun mediaType(): Try = - mediaTypeRetriever - .retrieve( - hints = MediaTypeHints(fileExtension = url.extension), - blob = this - ).tryRecover { error -> - when (error) { - is MediaTypeSnifferError.Read -> - Try.failure(error.cause) - MediaTypeSnifferError.NotRecognized -> - Try.success(MediaType.BINARY) - } - } - /** Plain text size. */ override suspend fun length(): Try { if (::_length.isInitialized) { diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index 87046bb640..9c5c19965b 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -77,7 +77,7 @@ internal class LicensesService( override fun contentProtection( authentication: LcpAuthenticating ): ContentProtection = - LcpContentProtection(this, authentication, assetRetriever, mediaTypeRetriever) + LcpContentProtection(this, authentication, assetRetriever) override fun publicationRetriever(): LcpPublicationRetriever { return LcpPublicationRetriever( diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt index 618edfd117..7d4a8a1262 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.publication.services.isProtected 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 +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.TransformingResource @@ -29,17 +29,15 @@ import timber.log.Timber @OptIn(ExperimentalReadiumApi::class) internal fun Resource.injectHtml( publication: Publication, + mediaType: MediaType, css: ReadiumCss, baseHref: AbsoluteUrl, disableSelectionWhenProtected: Boolean ): Resource = TransformingResource(this) { bytes -> - val mediaType = mediaType() - .getOrElse { - return@TransformingResource ResourceTry.failure(it) - } - .takeIf { it.isHtml } - ?: return@TransformingResource ResourceTry.success(bytes) + if (!mediaType.isHtml) { + return@TransformingResource ResourceTry.success(bytes) + } var content = bytes.toString(mediaType.charset ?: Charsets.UTF_8).trim() val injectables = mutableListOf() diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt index bbc4d5ed2d..290803fae4 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt @@ -21,12 +21,11 @@ import org.readium.r2.shared.publication.Publication 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.data.BlobInputStream import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.data.ReadableInputStream import org.readium.r2.shared.util.http.HttpHeaders import org.readium.r2.shared.util.http.HttpRange -import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.StringResource import org.readium.r2.shared.util.resource.fallback @@ -105,14 +104,17 @@ internal class WebViewServer( errorResource(urlWithoutAnchor, error) } - if (link.mediaType?.isHtml == true) { - resource = resource.injectHtml( - publication, - css, - baseHref = assetsBaseHref, - disableSelectionWhenProtected = disableSelectionWhenProtected - ) - } + link.mediaType + ?.takeIf { it.isHtml } + ?.let { + resource = resource.injectHtml( + publication, + mediaType = it, + css, + baseHref = assetsBaseHref, + disableSelectionWhenProtected = disableSelectionWhenProtected + ) + } val headers = mutableMapOf( "Accept-Ranges" to "bytes" @@ -125,10 +127,10 @@ internal class WebViewServer( 200, "OK", headers, - BlobInputStream(resource, ::ReadException) + ReadableInputStream(resource, ::ReadException) ) } else { // Byte range request - val stream = BlobInputStream(resource, ::ReadException) + val stream = ReadableInputStream(resource, ::ReadException) val length = stream.available() val longRange = range.toLongRange(length.toLong()) headers["Content-Range"] = "bytes ${longRange.first}-${longRange.last}/$length" @@ -146,7 +148,7 @@ internal class WebViewServer( } } private fun errorResource(url: Url, error: ReadError): Resource = - StringResource(mediaType = MediaType.XHTML) { + StringResource { withContext(Dispatchers.IO) { Try.success( application.assets diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index 4f8072fc28..c68dd49189 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -38,9 +38,7 @@ import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpStreamResponse -import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.withMediaType internal typealias ServiceFactory = (Publication.Service.Context) -> Publication.Service? @@ -201,22 +199,14 @@ public class Publication( * Returns the resource targeted by the given non-templated [link]. */ public fun get(link: Link): Resource? = - get(link.url(), link.mediaType) + get(link.url()) /** * Returns the resource targeted by the given [href]. */ public fun get(href: Url): Resource? = - get(href, linkWithHref(href)?.mediaType) - - private fun get(href: Url, mediaType: MediaType?): Resource? { - val entry = container.get(href) - ?: container.get(href.removeQuery().removeFragment()) // Try again after removing query and fragment. - ?: return null - - return entry - .withMediaType(mediaType) - } + // Try first the original href and falls back to href without query and fragment. + container[href] ?: container[href.removeQuery().removeFragment()] /** * Closes any opened resource associated with the [Publication], including services. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt index eda7daa799..a0869236d4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt @@ -66,9 +66,10 @@ public class HtmlResourceContentIterator internal constructor( servicesHolder: PublicationServicesHolder, readingOrderIndex: Int, resource: Resource, + mediaType: MediaType, locator: Locator ): Content.Iterator? { - if (resource.mediaType().getOrNull()?.matchesAny(MediaType.HTML, MediaType.XHTML) != true) { + if (!mediaType.matchesAny(MediaType.HTML, MediaType.XHTML)) { return null } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt index ec1e4cc268..b7f77a29cd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt @@ -15,6 +15,7 @@ import org.readium.r2.shared.publication.PublicationServicesHolder import org.readium.r2.shared.publication.indexOfFirstWithHref import org.readium.r2.shared.publication.services.content.Content import org.readium.r2.shared.util.Either +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource /** @@ -36,6 +37,7 @@ public fun interface ResourceContentIteratorFactory { servicesHolder: PublicationServicesHolder, readingOrderIndex: Int, resource: Resource, + mediaType: MediaType, locator: Locator ): Content.Iterator? } @@ -164,11 +166,12 @@ public class PublicationContentIterator( private suspend fun loadIteratorAt(index: Int, location: LocatorOrProgression): IndexedIterator? { val link = manifest.readingOrder[index] val locator = location.toLocator(link) ?: return null - val resource = container.get(link.url()) ?: return null + val resource = container[link.url()] ?: return null + val mediaType = link.mediaType ?: return null return resourceContentIteratorFactories .firstNotNullOfOrNull { factory -> - factory.create(manifest, services, index, resource, locator) + factory.create(manifest, services, index, resource, mediaType, locator) } ?.let { IndexedIterator(index, it) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt index 442974b4e1..0bf82e644b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt @@ -114,9 +114,10 @@ public class StringSearchService( index += 1 val link = manifest.readingOrder[index] + val mediaType = link.mediaType ?: return next() val text = - container.get(link.url()) - ?.let { extractorFactory.createExtractor(it)?.extractText(it) } + container[link.url()] + ?.let { extractorFactory.createExtractor(it, mediaType)?.extractText(it) } ?.getOrElse { return Try.failure(SearchError.ResourceError(it)) } if (text == null) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 44ad50ea3f..42b763f7f9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -14,12 +14,10 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.getOrElse 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.resource.ArchiveFactory import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.SmartArchiveFactory -import org.readium.r2.shared.util.resource.invoke import org.readium.r2.shared.util.toUrl /** @@ -111,26 +109,21 @@ public class AssetRetriever( ) } - val properties = resource.properties() - .getOrElse { return Try.failure(Error.ReadError(it)) } - - val mediaType = mediaTypeRetriever.retrieve( - MediaTypeHints(properties), - resource - ).getOrElse { - return Try.failure( - Error.FormatNotSupported( - MessageError("Cannot determine asset media type.") + val mediaType = mediaTypeRetriever.retrieve(resource) + .getOrElse { + return Try.failure( + Error.FormatNotSupported( + MessageError("Cannot determine asset media type.") + ) ) - ) - } + } val container = archiveFactory.create(mediaType, resource) .getOrElse { when (it) { is ArchiveFactory.Error.ReadError -> return Try.failure(Error.ReadError(it.cause)) - else -> + is ArchiveFactory.Error.FormatNotSupported -> return Try.success(Asset.Resource(mediaType, resource)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt index dc17d9f88e..c7f9bbc70d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt @@ -7,25 +7,18 @@ package org.readium.r2.shared.util.asset import android.content.ContentResolver -import android.provider.MediaStore -import org.readium.r2.shared.extensions.queryProjection import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.ContentBlob import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.BlobResourceAdapter -import org.readium.r2.shared.util.resource.MediaTypeRetriever +import org.readium.r2.shared.util.resource.ContentResource import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.filename -import org.readium.r2.shared.util.resource.mediaType import org.readium.r2.shared.util.toUri /** - * Creates [ContentBlob]s. + * Creates [ContentResource]s. */ public class ContentResourceFactory( - private val contentResolver: ContentResolver, - private val mediaTypeRetriever: MediaTypeRetriever + private val contentResolver: ContentResolver ) : ResourceFactory { override suspend fun create( @@ -36,26 +29,7 @@ public class ContentResourceFactory( return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) } - val blob = ContentBlob(url.toUri(), contentResolver) - - val filename = - contentResolver.queryProjection(url.uri, MediaStore.MediaColumns.DISPLAY_NAME) - - val properties = - Resource.Properties( - Resource.Properties.Builder() - .also { - it.filename = filename - it.mediaType = mediaType - } - ) - - val resource = - BlobResourceAdapter( - blob, - properties, - mediaTypeRetriever - ) + val resource = ContentResource(url.toUri(), contentResolver) return Try.success(resource) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt index f0d5269fb3..47c382e447 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt @@ -8,17 +8,11 @@ package org.readium.r2.shared.util.asset import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.BlobResourceAdapter -import org.readium.r2.shared.util.resource.MediaTypeRetriever +import org.readium.r2.shared.util.resource.FileResource import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.filename -import org.readium.r2.shared.util.resource.mediaType -public class FileResourceFactory( - private val mediaTypeRetriever: MediaTypeRetriever -) : ResourceFactory { +public class FileResourceFactory : ResourceFactory { override suspend fun create( url: AbsoluteUrl, @@ -27,23 +21,7 @@ public class FileResourceFactory( val file = url.toFile() ?: return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) - val blob = FileBlob(file) - - val properties = - Resource.Properties( - Resource.Properties.Builder() - .also { - it.filename = url.filename - it.mediaType = mediaType - } - ) - - val resource = - BlobResourceAdapter( - blob, - properties, - mediaTypeRetriever - ) + val resource = FileResource(file, mediaType) return Try.success(resource) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt index f52b33caa5..e19b48a55d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt @@ -11,12 +11,10 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpResource import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource public class HttpResourceFactory( - private val httpClient: HttpClient, - private val mediaTypeRetriever: MediaTypeRetriever + private val httpClient: HttpClient ) : ResourceFactory { override suspend fun create( @@ -27,7 +25,7 @@ public class HttpResourceFactory( return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) } - val resource = HttpResource(url, httpClient, mediaTypeRetriever) + val resource = HttpResource(url, httpClient) return Try.success(resource) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index 4b0dc356cf..f4f6c25cbf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -14,7 +14,7 @@ import org.readium.r2.shared.util.resource.Resource /** * A container provides access to a list of [Resource] entries. */ -public interface Container : Iterable, SuspendingCloseable { +public interface Container : Iterable, SuspendingCloseable { /** * Direct source to this container, when available. @@ -36,7 +36,7 @@ public interface Container : Iterable, SuspendingCloseable { } /** A [Container] providing no resources at all. */ -public class EmptyContainer : +public class EmptyContainer : Container { override val entries: Set = emptySet() @@ -54,7 +54,7 @@ public class EmptyContainer : * * The [containers] will be tested in the given order. */ -public class CompositeContainer( +public class CompositeContainer( private val containers: List> ) : Container { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index 4ba5fecb47..b0eacfa5e1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -80,7 +80,7 @@ internal suspend fun Try.decodeMap( * It will extract the charset parameter from the media type hints to figure out an encoding. * Otherwise, fallback on UTF-8. */ -public suspend fun Blob.readAsString( +public suspend fun Readable.readAsString( charset: Charset = Charsets.UTF_8 ): Try = read().decode( @@ -89,7 +89,7 @@ public suspend fun Blob.readAsString( ) /** Content as an XML document. */ -public suspend fun Blob.readAsXml(): Try = +public suspend fun Readable.readAsXml(): Try = read().decode( { XmlParser().parse(ByteArrayInputStream(it)) }, { MessageError("Content is not a valid XML document.", ThrowableError(it)) } @@ -98,14 +98,14 @@ public suspend fun Blob.readAsXml(): Try = /** * Content parsed from JSON. */ -public suspend fun Blob.readAsJson(): Try = +public suspend fun Readable.readAsJson(): Try = readAsString().decodeMap( { JSONObject(it) }, { MessageError("Content is not valid JSON.", ThrowableError(it)) } ) /** Readium Web Publication Manifest parsed from the content. */ -public suspend fun Blob.readAsRwpm(): Try = +public suspend fun Readable.readAsRwpm(): Try = readAsJson().flatMap { json -> Manifest.fromJSON(json) ?.let { Try.success(it) } @@ -119,7 +119,7 @@ public suspend fun Blob.readAsRwpm(): Try = /** * Reads the full content as a [Bitmap]. */ -public suspend fun Blob.readAsBitmap(): Try = +public suspend fun Readable.readAsBitmap(): Try = read() .mapFailure { DecoderError.Read(it) } .flatMap { bytes -> @@ -135,7 +135,7 @@ public suspend fun Blob.readAsBitmap(): Try = /** * Returns whether the content is a JSON object containing all of the given root keys. */ -public suspend fun Blob.containsJsonKeys( +public suspend fun Readable.containsJsonKeys( vararg keys: String ): Try { val json = readAsJson() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Blob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Readable.kt similarity index 84% rename from readium/shared/src/main/java/org/readium/r2/shared/util/data/Blob.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/Readable.kt index 2c3354683c..9eb712b2f7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Blob.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Readable.kt @@ -6,19 +6,13 @@ package org.readium.r2.shared.util.data -import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Try /** * Acts as a proxy to an actual data source by handling read access. */ -public interface Blob : SuspendingCloseable { - - /** - * URL locating this resource, if any. - */ - public val source: AbsoluteUrl? +public interface Readable : SuspendingCloseable { /** * Returns data length from metadata if available, or calculated from reading the bytes otherwise. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadableInputStream.kt similarity index 92% rename from readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadableInputStream.kt index 1d69c8becf..c3ff78c4a7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/BlobInputStream.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadableInputStream.kt @@ -12,13 +12,13 @@ import kotlinx.coroutines.runBlocking import org.readium.r2.shared.util.Try /** - * Input stream reading through a [Blob]. + * Input stream reading through a [Readable]. * * If you experience bad performances, consider wrapping the stream in a BufferedInputStream. This * is particularly useful when streaming deflated ZIP entries. */ -public class BlobInputStream( - private val blob: Blob, +public class ReadableInputStream( + private val readable: Readable, private val wrapError: (ReadError) -> IOException, private val range: LongRange? = null ) : InputStream() { @@ -27,7 +27,7 @@ public class BlobInputStream( private val end: Long by lazy { val resourceLength = - runBlocking { blob.length() } + runBlocking { readable.length() } .recover() if (range == null) { @@ -67,7 +67,7 @@ public class BlobInputStream( } val bytes = runBlocking { - blob.read(position until (position + 1)) + readable.read(position until (position + 1)) .recover() } position += 1 @@ -83,7 +83,7 @@ public class BlobInputStream( val bytesToRead = len.coerceAtMost(available()) val bytes = runBlocking { - blob.read(position until (position + bytesToRead)) + readable.read(position until (position + bytesToRead)) .recover() } check(bytes.size <= bytesToRead) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index 141bdaac0e..783e4b6eba 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.tryOr import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpError @@ -254,7 +253,7 @@ public class AndroidDownloadManager internal constructor( SystemDownloadManager.STATUS_SUCCESSFUL -> { prepareResult( Uri.parse(facade.localUri!!)!!.toFile(), - mediaTypeHint = facade.mediaType + mediaTypeHint = facade.mediaType?.let { MediaType(it) } ) .onSuccess { download -> listenersForId.forEach { it.onDownloadCompleted(id, download) } @@ -273,15 +272,11 @@ public class AndroidDownloadManager internal constructor( } } - private suspend fun prepareResult(destFile: File, mediaTypeHint: String?): Try = + private suspend fun prepareResult(destFile: File, mediaTypeHint: MediaType?): Try = withContext(Dispatchers.IO) { val mediaType = mediaTypeRetriever.retrieve( - hints = MediaTypeHints( - mediaTypes = listOfNotNull( - mediaTypeHint?.let { MediaType(it) } - ) - ), - blob = FileBlob(destFile) + destFile, + MediaTypeHints(mediaType = mediaTypeHint) ).getOrElse { MediaType.BINARY } val extension = formatRegistry.fileExtension(mediaType) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index da3e961918..beda2ab935 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -26,13 +26,17 @@ import org.readium.r2.shared.util.MessageError 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.data.InMemoryBlob import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrDefault import org.readium.r2.shared.util.http.HttpRequest.Method import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints +import org.readium.r2.shared.util.resource.InMemoryResource import org.readium.r2.shared.util.resource.MediaTypeRetriever +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.resource.mediaType +import org.readium.r2.shared.util.toAbsoluteUrl import org.readium.r2.shared.util.toDebugDescription import org.readium.r2.shared.util.tryRecover import timber.log.Timber @@ -176,10 +180,23 @@ public class DefaultHttpClient( // Reads the full body, since it might contain an error representation such as // JSON Problem Details or OPDS Authentication Document val body = connection.errorStream?.use { it.readBytes() } + + val resourceProperties = + Resource.Properties( + Resource.Properties.Builder() + .apply { + mediaType = connection.contentType?.let { MediaType(it) } + filename = connection.url.file + } + + ) val mediaType = body?.let { mediaTypeRetriever.retrieve( - hints = MediaTypeHints(connection), - blob = InMemoryBlob(it) + InMemoryResource( + it, + connection.url.toAbsoluteUrl(), + resourceProperties + ) ).getOrDefault(MediaType.BINARY) } return@withContext Try.failure( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt index 571393fdab..1f5843ce1e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt @@ -9,7 +9,6 @@ package org.readium.r2.shared.util.http import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource /** @@ -24,8 +23,7 @@ import org.readium.r2.shared.util.resource.Resource public class HttpContainer( private val baseUrl: Url? = null, override val entries: Set, - private val client: HttpClient, - private val mediaTypeRetriever: MediaTypeRetriever + private val client: HttpClient ) : Container { override fun get(url: Url): Resource? { @@ -36,7 +34,7 @@ public class HttpContainer( return if (absoluteUrl == null || !absoluteUrl.isHttp) { null } else { - HttpResource(absoluteUrl, client, mediaTypeRetriever) + HttpResource(absoluteUrl, client) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index d9a9889093..bd1bc9abf4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -18,50 +18,26 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap -import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.invoke +import org.readium.r2.shared.util.resource.filename import org.readium.r2.shared.util.resource.mediaType -import org.readium.r2.shared.util.tryRecover /** Provides access to an external URL through HTTP. */ @OptIn(ExperimentalReadiumApi::class) public class HttpResource( override val source: AbsoluteUrl, private val client: HttpClient, - private val mediaTypeRetriever: MediaTypeRetriever, private val maxSkipBytes: Long = MAX_SKIP_BYTES ) : Resource { - override suspend fun mediaType(): Try { - val properties = properties() - .getOrElse { return Try.failure(it) } - - val mediaTypeHints = - MediaTypeHints(properties) - - return mediaTypeRetriever.retrieve(mediaTypeHints, this) - .tryRecover { - when (it) { - MediaTypeSnifferError.NotRecognized -> - Try.success(MediaType.BINARY) - is MediaTypeSnifferError.Read -> - Try.failure(it.cause) - } - } - } - override suspend fun properties(): Try = headResponse().map { Resource.Properties( Resource.Properties.Builder() .apply { mediaType = it.mediaType + filename = it.url.filename } ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt index c353666167..015688835a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt @@ -7,8 +7,8 @@ package org.readium.r2.shared.util.mediatype import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.Readable /** * The default composite sniffer provided by Readium for all known formats. @@ -38,8 +38,8 @@ public class DefaultMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try = sniffer.sniffHints(hints) - override suspend fun sniffBlob(blob: Blob): Try = - sniffer.sniffBlob(blob) + override suspend fun sniffBlob(readable: Readable): Try = + sniffer.sniffBlob(readable) override suspend fun sniffContainer(container: Container<*>): Try = sniffer.sniffContainer(container) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt index 7da8aa8494..c34f7f1a31 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt @@ -77,4 +77,7 @@ public class FormatRegistry( public fun superType(mediaType: MediaType): MediaType? = superTypes[mediaType] + + public fun isSuperType(mediaType: MediaType): Boolean = + superTypes.values.any { it.matches(mediaType) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index 6a9bfc5bdb..9211d621e8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -22,11 +22,11 @@ import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.Blob -import org.readium.r2.shared.util.data.BlobInputStream import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.DecoderError import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.data.ReadableInputStream import org.readium.r2.shared.util.data.containsJsonKeys import org.readium.r2.shared.util.data.readAsJson import org.readium.r2.shared.util.data.readAsRwpm @@ -53,7 +53,7 @@ public interface HintMediaTypeSniffer { public interface BlobMediaTypeSniffer { public suspend fun sniffBlob( - blob: Blob + readable: Readable ): Try } @@ -80,10 +80,10 @@ public interface MediaTypeSniffer : Try.failure(MediaTypeSnifferError.NotRecognized) /** - * Sniffs a [MediaType] from a [Blob]. + * Sniffs a [MediaType] from a [Readable]. */ public override suspend fun sniffBlob( - blob: Blob + readable: Readable ): Try = Try.failure(MediaTypeSnifferError.NotRecognized) @@ -112,9 +112,9 @@ public class CompositeMediaTypeSniffer( return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { + override suspend fun sniffBlob(readable: Readable): Try { for (sniffer in sniffers) { - sniffer.sniffBlob(blob) + sniffer.sniffBlob(readable) .getOrElse { error -> when (error) { MediaTypeSnifferError.NotRecognized -> @@ -164,12 +164,12 @@ public object XhtmlMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { - if (!blob.canReadWholeBlob()) { + override suspend fun sniffBlob(readable: Readable): Try { + if (!readable.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } - blob.readAsXml() + readable.readAsXml() .getOrElse { when (it) { is DecoderError.Read -> @@ -204,13 +204,13 @@ public object HtmlMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { - if (!blob.canReadWholeBlob()) { + override suspend fun sniffBlob(readable: Readable): Try { + if (!readable.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } // [contentAsXml] will fail if the HTML is not a proper XML document, hence the doctype check. - blob.readAsXml() + readable.readAsXml() .getOrElse { when (it) { is DecoderError.Read -> @@ -222,7 +222,7 @@ public object HtmlMediaTypeSniffer : MediaTypeSniffer { ?.takeIf { it.name.lowercase(Locale.ROOT) == "html" } ?.let { return Try.success(MediaType.HTML) } - blob.readAsString() + readable.readAsString() .getOrElse { when (it) { is DecoderError.Read -> @@ -276,13 +276,13 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { - if (!blob.canReadWholeBlob()) { + override suspend fun sniffBlob(readable: Readable): Try { + if (!readable.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } // OPDS 1 - blob.readAsXml() + readable.readAsXml() .getOrElse { when (it) { is DecoderError.Read -> @@ -301,7 +301,7 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { } // OPDS 2 - blob.readAsRwpm() + readable.readAsRwpm() .getOrElse { when (it) { is DecoderError.Read -> @@ -333,7 +333,7 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { } // OPDS Authentication Document. - blob.containsJsonKeys("id", "title", "authentication") + readable.containsJsonKeys("id", "title", "authentication") .getOrElse { when (it) { is DecoderError.Read -> @@ -363,12 +363,12 @@ public object LcpLicenseMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { - if (!blob.canReadWholeBlob()) { + override suspend fun sniffBlob(readable: Readable): Try { + if (!readable.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } - blob.containsJsonKeys("id", "issued", "provider", "encryption") + readable.containsJsonKeys("id", "issued", "provider", "encryption") .getOrElse { when (it) { is DecoderError.Read -> @@ -458,13 +458,13 @@ public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - public override suspend fun sniffBlob(blob: Blob): Try { - if (!blob.canReadWholeBlob()) { + public override suspend fun sniffBlob(readable: Readable): Try { + if (!readable.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } val manifest: Manifest = - blob.readAsRwpm() + readable.readAsRwpm() .getOrElse { when (it) { is DecoderError.Read -> @@ -567,13 +567,13 @@ public object WebPubMediaTypeSniffer : MediaTypeSniffer { /** Sniffs a W3C Web Publication Manifest. */ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { - override suspend fun sniffBlob(blob: Blob): Try { - if (!blob.canReadWholeBlob()) { + override suspend fun sniffBlob(readable: Readable): Try { + if (!readable.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. - val string = blob.readAsString() + val string = readable.readAsString() .getOrElse { when (it) { is DecoderError.Read -> @@ -783,8 +783,8 @@ public object PdfMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { - blob.read(0L until 5L) + override suspend fun sniffBlob(readable: Readable): Try { + readable.read(0L until 5L) .getOrElse { error -> return Try.failure(MediaTypeSnifferError.Read(error)) } @@ -806,12 +806,12 @@ public object JsonMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { - if (!blob.canReadWholeBlob()) { + override suspend fun sniffBlob(readable: Readable): Try { + if (!readable.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } - blob.readAsJson() + readable.readAsJson() .getOrElse { when (it) { is DecoderError.Read -> @@ -849,8 +849,8 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { - BlobInputStream(blob, ::SystemSnifferException) + override suspend fun sniffBlob(readable: Readable): Try { + ReadableInputStream(readable, ::SystemSnifferException) .use { stream -> try { withContext(Dispatchers.IO) { @@ -888,5 +888,5 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { ?.let { MediaType(it) } } -private suspend fun Blob.canReadWholeBlob() = +private suspend fun Readable.canReadWholeBlob() = length().getOrDefault(0) < 5 * 1000 * 1000 diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt index e1358950f5..c30af9d452 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt @@ -7,13 +7,13 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType /** - * A factory to create a [ResourceContainer]s from archive [Blob]s. + * A factory to create a [ResourceContainer]s from archive [Readable]s. */ public interface ArchiveFactory { @@ -36,7 +36,7 @@ public interface ArchiveFactory { */ public suspend fun create( mediaType: MediaType, - blob: Blob + readable: Readable ): Try, Error> } @@ -49,10 +49,10 @@ public class CompositeArchiveFactory( override suspend fun create( mediaType: MediaType, - blob: Blob + readable: Readable ): Try, ArchiveFactory.Error> { for (factory in factories) { - factory.create(mediaType, blob) + factory.create(mediaType, readable) .getOrElse { error -> when (error) { is ArchiveFactory.Error.FormatNotSupported -> null diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobMediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobMediaTypeRetriever.kt index 7058e78a25..695c4cfd91 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobMediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobMediaTypeRetriever.kt @@ -12,7 +12,7 @@ import java.io.File import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.extensions.queryProjection import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeSniffer @@ -45,8 +45,8 @@ public class BlobMediaTypeRetriever( return hints.mediaTypes.firstOrNull() } - public suspend fun retrieve(hints: MediaTypeHints, blob: Blob): Try { - mediaTypeSniffer.sniffBlob(blob) + public suspend fun retrieve(hints: MediaTypeHints, readable: Readable): Try { + mediaTypeSniffer.sniffBlob(readable) .onSuccess { return Try.success(it) } .onFailure { error -> when (error) { @@ -63,7 +63,7 @@ public class BlobMediaTypeRetriever( .getOrNull() ?.let { return Try.success(it) } - SystemMediaTypeSniffer.sniffBlob(blob) + SystemMediaTypeSniffer.sniffBlob(readable) .onSuccess { return Try.success(it) } .onFailure { error -> when (error) { @@ -78,7 +78,7 @@ public class BlobMediaTypeRetriever( // their content (for example, for RWPM). if (contentResolver != null) { - blob.source + (readable as Resource).source ?.takeIf { it.isContent } ?.let { url -> val contentHints = MediaTypeHints( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt deleted file mode 100644 index 4dd585b2ad..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobResourceAdapter.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.resource - -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.Blob -import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.tryRecover - -internal class BlobResourceAdapter( - private val blob: Blob, - properties: Resource.Properties, - private val mediaTypeRetriever: MediaTypeRetriever -) : Resource, Blob by blob { - - private val properties: Resource.Properties = - properties.copy { mediaType = mediaTypeRetriever.retrieve(MediaTypeHints(properties)) } - - override suspend fun mediaType(): Try = - mediaTypeRetriever.retrieve( - hints = MediaTypeHints(properties), - blob = blob - ).tryRecover { error -> - when (error) { - is MediaTypeSnifferError.Read -> - Try.failure(error.cause) - MediaTypeSnifferError.NotRecognized -> - Try.success(MediaType.BINARY) - } - } - - override suspend fun properties(): Try = - Try.success( - Resource.Properties(properties) - ) -} - -internal class BlobContainerAdapter( - private val container: Container, - private val properties: Map, - private val mediaTypeRetriever: MediaTypeRetriever -) : Container { - override val entries: Set = - container.entries - - override fun get(url: Url): Resource? { - val blob = container[url] ?: return null - - val resourceProperties = properties[url] ?: Resource.Properties() - - return BlobResourceAdapter(blob, resourceProperties, mediaTypeRetriever) - } - - override suspend fun close() { - container.close() - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt similarity index 80% rename from readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt index c3873f5599..d31cda5d44 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentBlob.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt @@ -4,10 +4,11 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.data +package org.readium.r2.shared.util.resource import android.content.ContentResolver import android.net.Uri +import android.provider.MediaStore import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream @@ -18,24 +19,44 @@ import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ContentProviderError +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.toUrl /** - * A [Blob] to access content [uri] thanks to a [ContentResolver]. + * A [Resource] to access content [uri] thanks to a [ContentResolver]. */ -public class ContentBlob( +public class ContentResource( private val uri: Uri, - private val contentResolver: ContentResolver -) : Blob { + private val contentResolver: ContentResolver, + private val mediaType: MediaType? = null +) : Resource { private lateinit var _length: Try + private val filename = + contentResolver.queryProjection(uri, MediaStore.MediaColumns.DISPLAY_NAME) + + private val properties = + Resource.Properties( + Resource.Properties.Builder() + .also { + it.filename = filename + it.mediaType = mediaType + } + ) + override val source: AbsoluteUrl? = uri.toUrl() as? AbsoluteUrl override suspend fun close() { } + override suspend fun properties(): Try { + return Try.success(properties) + } + override suspend fun read(range: LongRange?): Try { if (range == null) { return readFully() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index bf6b54709d..9b687858b0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -13,7 +13,6 @@ import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.toUrl @@ -22,32 +21,20 @@ import org.readium.r2.shared.util.toUrl */ public class DirectoryContainer( private val root: File, - private val mediaTypeRetriever: MediaTypeRetriever, override val entries: Set ) : Container { - private fun File.toResource(): Resource { - return BlobResourceAdapter( - FileBlob(this), - Resource.Properties( - Resource.Properties.Builder() - .also { it.filename = name } - ), - mediaTypeRetriever - ) - } - override fun get(url: Url): Resource? = url .takeIf { it in entries } ?.let { (it as? RelativeUrl)?.path } ?.let { File(root, it) } - ?.toResource() + ?.let { FileResource(it) } override suspend fun close() {} public companion object { - public suspend operator fun invoke(root: File, mediaTypeRetriever: MediaTypeRetriever): Try { + public suspend operator fun invoke(root: File): Try { val entries = try { withContext(Dispatchers.IO) { @@ -59,7 +46,7 @@ public class DirectoryContainer( } catch (e: SecurityException) { return Try.failure(FileSystemError.Forbidden(e)) } - val container = DirectoryContainer(root, mediaTypeRetriever, entries) + val container = DirectoryContainer(root, entries) return Try.success(container) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt index 31abd044c8..b603847d1d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt @@ -9,7 +9,6 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.mediatype.MediaType /** * Resource that will act as a proxy to a fallback resource if the [originalResource] errors out. @@ -21,9 +20,6 @@ public class FallbackResource( override val source: AbsoluteUrl? = null - override suspend fun mediaType(): Try = - withResource { mediaType() } - override suspend fun properties(): Try = withResource { properties() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt similarity index 82% rename from readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt index 8af2eaecbf..dd3a01596a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileBlob.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.data +package org.readium.r2.shared.util.resource import java.io.File import java.io.FileNotFoundException @@ -17,16 +17,20 @@ import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.FileSystemError +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.isLazyInitialized +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.toUrl /** - * A [Blob] to access a [File]. + * A [Resource] to access a [File]. */ -public class FileBlob( - private val file: File -) : Blob { +public class FileResource( + private val file: File, + private val mediaType: MediaType? = null +) : Resource { private val randomAccessFile by lazy { try { @@ -36,8 +40,21 @@ public class FileBlob( } } + private val properties = + Resource.Properties( + Resource.Properties.Builder() + .also { + it.filename = file.name + it.mediaType = mediaType + } + ) + override val source: AbsoluteUrl = file.toUrl() + public override suspend fun properties(): Try { + return Try.success(properties) + } + override suspend fun close() { withContext(Dispatchers.IO) { if (::randomAccessFile.isLazyInitialized) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/InMemoryBlob.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/InMemoryResource.kt similarity index 72% rename from readium/shared/src/main/java/org/readium/r2/shared/util/data/InMemoryBlob.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/resource/InMemoryResource.kt index 8c661a44ee..7828283e3b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/InMemoryBlob.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/InMemoryResource.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.data +package org.readium.r2.shared.util.resource import kotlinx.coroutines.runBlocking import org.readium.r2.shared.extensions.coerceFirstNonNegative @@ -12,20 +12,27 @@ import org.readium.r2.shared.extensions.read import org.readium.r2.shared.extensions.requireLengthFitInt import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.ReadError -/** Creates a [Blob] serving a [ByteArray]. */ -public class InMemoryBlob( +/** Creates a [Resource] serving a [ByteArray]. */ +public class InMemoryResource( override val source: AbsoluteUrl?, + private val properties: Resource.Properties, private val bytes: suspend () -> Try -) : Blob { +) : Resource { public constructor( bytes: ByteArray, - source: AbsoluteUrl? = null - ) : this(source = source, { Try.success(bytes) }) + source: AbsoluteUrl? = null, + properties: Resource.Properties = Resource.Properties() + ) : this(source = source, properties = properties, { Try.success(bytes) }) private lateinit var _bytes: Try + override suspend fun properties(): Try { + return Try.success(properties) + } + override suspend fun length(): Try = read().map { it.size.toLong() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt index 52d66953bb..9c073db2a5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt @@ -9,7 +9,6 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.mediatype.MediaType /** * Wraps a [Resource] which will be created only when first accessing one of its members. @@ -29,9 +28,6 @@ public open class LazyResource( return _resource } - override suspend fun mediaType(): Try = - resource().mediaType() - override suspend fun properties(): Try = resource().properties() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt index f6a2a10652..fe65de63cf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt @@ -10,9 +10,8 @@ import android.content.ContentResolver import java.io.File import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.FileBlob +import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer import org.readium.r2.shared.util.mediatype.FormatRegistry @@ -21,6 +20,7 @@ import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeSniffer import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.mediatype.SystemMediaTypeSniffer +import org.readium.r2.shared.util.use import org.readium.r2.shared.util.zip.ZipArchiveFactory /** @@ -48,7 +48,7 @@ public class MediaTypeRetriever( DefaultMediaTypeSniffer() val archiveFactory = - ZipArchiveFactory(mediaTypeSniffer) + ZipArchiveFactory() val formatRegistry = FormatRegistry() @@ -113,8 +113,8 @@ public class MediaTypeRetriever( retrieve(MediaTypeHints(mediaTypes = mediaTypes, fileExtensions = fileExtensions)) public suspend fun retrieve( - hints: MediaTypeHints = MediaTypeHints(), - container: Container<*> + container: Container, + hints: MediaTypeHints = MediaTypeHints() ): Try { mediaTypeSniffer.sniffHints(hints) .getOrNull() @@ -134,37 +134,39 @@ public class MediaTypeRetriever( ?: Try.failure(MediaTypeSnifferError.NotRecognized) } - public suspend fun retrieve(file: File): Try = - retrieve( - hints = MediaTypeHints(fileExtension = file.extension), - blob = FileBlob(file) - ) + public suspend fun retrieve( + file: File, + hints: MediaTypeHints = MediaTypeHints() + ): Try = + FileResource(file).use { retrieve(it, hints) } /** - * Retrieves a canonical [MediaType] for the provided media type and file extensions [hints] and - * asset [blob]. + * Retrieves a canonical [MediaType] for [resource]. */ public suspend fun retrieve( - hints: MediaTypeHints = MediaTypeHints(), - blob: Blob + resource: Resource, + hints: MediaTypeHints = MediaTypeHints() ): Try { - mediaTypeSniffer.sniffHints(hints) + val properties = resource.properties() + .getOrElse { return Try.failure(MediaTypeSnifferError.Read(it)) } + + mediaTypeSniffer.sniffHints(MediaTypeHints(properties) + hints) .getOrNull() ?.let { return Try.success(it) } - val blobMediaType = blobMediaTypeRetriever.retrieve(hints, blob) + val blobMediaType = blobMediaTypeRetriever.retrieve(hints, resource) .getOrElse { return Try.failure(it) } - val container = archiveFactory.create(blobMediaType, blob) + val container = archiveFactory.create(blobMediaType, resource) .getOrElse { when (it) { is ArchiveFactory.Error.ReadError -> return Try.failure(MediaTypeSnifferError.Read(it.cause)) - else -> + is ArchiveFactory.Error.FormatNotSupported -> return Try.success(blobMediaType) } } - return retrieve(hints, container) + return retrieve(container, hints) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index c10cd86a7a..b243d130bb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -8,10 +8,9 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.data.Readable public typealias ResourceTry = Try @@ -20,12 +19,12 @@ public typealias ResourceContainer = Container /** * Acts as a proxy to an actual resource by handling read access. */ -public interface Resource : Blob { +public interface Resource : Readable { /** - * Returns the resource media type if known. + * URL locating this resource, if any. */ - public suspend fun mediaType(): Try + public val source: AbsoluteUrl? /** * Properties associated to the resource. @@ -57,7 +56,6 @@ public class FailureResource( ) : Resource { override val source: AbsoluteUrl? = null - override suspend fun mediaType(): Try = Try.failure(error) override suspend fun properties(): Try = Try.failure(error) override suspend fun length(): Try = Try.failure(error) override suspend fun read(range: LongRange?): Try = Try.failure(error) @@ -79,14 +77,3 @@ public fun Try.mapCatching(): ResourceTry = @Suppress("UnusedReceiverParameter") public fun Try.flatMapCatching(): ResourceTry = throw NotImplementedError() - -internal fun Resource.withMediaType(mediaType: MediaType?): Resource { - if (mediaType == null) { - return this - } - - return object : Resource by this { - override suspend fun mediaType(): Try = - Try.success(mediaType) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt index 03df02a9f3..86f9509e55 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt @@ -7,8 +7,8 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.tryRecover @@ -20,14 +20,14 @@ internal class SmartArchiveFactory( override suspend fun create( mediaType: MediaType, - blob: Blob + readable: Readable ): Try, ArchiveFactory.Error> = - archiveFactory.create(mediaType, blob) + archiveFactory.create(mediaType, readable) .tryRecover { error -> when (error) { is ArchiveFactory.Error.FormatNotSupported -> { formatRegistry.superType(mediaType) - ?.let { archiveFactory.create(it, blob) } + ?.let { archiveFactory.create(it, readable) } ?: Try.failure(error) } is ArchiveFactory.Error.ReadError -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt index ee7558295d..51530c5eca 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt @@ -9,34 +9,29 @@ package org.readium.r2.shared.util.resource import kotlinx.coroutines.runBlocking import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Blob -import org.readium.r2.shared.util.data.InMemoryBlob import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.data.Readable /** Creates a Resource serving a [String]. */ public class StringResource( - private val blob: Blob, - private val mediaType: MediaType, + private val readable: Readable, private val properties: Resource.Properties -) : Resource, Blob by blob { +) : Resource, Readable by readable { public constructor( - mediaType: MediaType, source: AbsoluteUrl? = null, properties: Resource.Properties = Resource.Properties(), string: suspend () -> Try - ) : this(InMemoryBlob(source) { string().map { it.toByteArray() } }, mediaType, properties) + ) : this(InMemoryResource(source, properties) { string().map { it.toByteArray() } }, properties) public constructor( string: String, - mediaType: MediaType, source: AbsoluteUrl? = null, properties: Resource.Properties = Resource.Properties() - ) : this(mediaType, source, properties, { Try.success(string) }) + ) : this(source, properties, { Try.success(string) }) - override suspend fun mediaType(): Try = - Try.success(mediaType) + override val source: AbsoluteUrl? = + null override suspend fun properties(): Try = Try.success(properties) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt index df7f8fc670..422c07202e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt @@ -11,7 +11,6 @@ import kotlinx.coroutines.sync.withLock import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.mediatype.MediaType /** * Protects the access to a wrapped resource with a mutex to make it thread-safe. @@ -29,9 +28,6 @@ public class SynchronizedResource( override suspend fun properties(): Try = mutex.withLock { resource.properties() } - override suspend fun mediaType(): Try = - mutex.withLock { resource.mediaType() } - override suspend fun length(): Try = mutex.withLock { resource.length() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt index 169012e2fd..cb008e373a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt @@ -36,15 +36,15 @@ public interface ResourceContentExtractor { * * Return null if the resource format is not supported. */ - public suspend fun createExtractor(resource: Resource): ResourceContentExtractor? + public suspend fun createExtractor(resource: Resource, mediaType: MediaType): ResourceContentExtractor? } } @ExperimentalReadiumApi public class DefaultResourceContentExtractorFactory : ResourceContentExtractor.Factory { - override suspend fun createExtractor(resource: Resource): ResourceContentExtractor? = - when (resource.mediaType().getOrNull()) { + override suspend fun createExtractor(resource: Resource, mediaType: MediaType): ResourceContentExtractor? = + when (mediaType) { MediaType.HTML, MediaType.XHTML -> HtmlResourceContentExtractor() else -> null } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index f403bd7c9d..d45ea4d0d8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -13,10 +13,8 @@ import java.util.zip.ZipException import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError @@ -24,24 +22,17 @@ import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.ArchiveFactory -import org.readium.r2.shared.util.resource.BlobMediaTypeRetriever import org.readium.r2.shared.util.resource.Resource /** * An [ArchiveFactory] to open local ZIP files with Java's [ZipFile]. */ -@OptIn(DelicateReadiumApi::class) -internal class FileZipArchiveProvider( - private val mediaTypeRetriever: BlobMediaTypeRetriever? = null -) { - - suspend fun sniffBlob(blob: Blob): Try { - val file = blob.source?.toFile() - ?: return Try.Failure(MediaTypeSnifferError.NotRecognized) +internal class FileZipArchiveProvider { + suspend fun sniffFile(file: File): Try { return withContext(Dispatchers.IO) { try { - FileZipContainer(ZipFile(file), file, mediaTypeRetriever) + FileZipContainer(ZipFile(file), file) Try.success(MediaType.ZIP) } catch (e: ZipException) { Try.failure(MediaTypeSnifferError.NotRecognized) @@ -63,7 +54,7 @@ internal class FileZipArchiveProvider( suspend fun create( mediaType: MediaType, - blob: Blob + file: File ): Try, ArchiveFactory.Error> { if (mediaType != MediaType.ZIP) { return Try.failure( @@ -73,13 +64,6 @@ internal class FileZipArchiveProvider( ) } - val file = blob.source?.toFile() - ?: return Try.Failure( - ArchiveFactory.Error.FormatNotSupported( - MessageError("Resource not supported because file cannot be directly accessed.") - ) - ) - val container = open(file) .getOrElse { return Try.failure(it) } @@ -90,7 +74,7 @@ internal class FileZipArchiveProvider( internal suspend fun open(file: File): Try, ArchiveFactory.Error> = withContext(Dispatchers.IO) { try { - val archive = FileZipContainer(ZipFile(file), file, mediaTypeRetriever) + val archive = FileZipContainer(ZipFile(file), file) Try.success(archive) } catch (e: FileNotFoundException) { Try.failure( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt index e643d2afbf..284d543e47 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt @@ -13,7 +13,6 @@ import java.util.zip.ZipException import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.extensions.readFully import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl @@ -26,20 +25,14 @@ import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.ArchiveProperties -import org.readium.r2.shared.util.resource.BlobMediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.toUrl -import org.readium.r2.shared.util.tryRecover -@OptIn(DelicateReadiumApi::class) + internal class FileZipContainer( private val archive: ZipFile, - file: File, - private val mediaTypeRetriever: BlobMediaTypeRetriever? + file: File ) : Container { private inner class Entry(private val url: Url, private val entry: ZipEntry) : @@ -47,19 +40,6 @@ internal class FileZipContainer( override val source: AbsoluteUrl? = null - override suspend fun mediaType(): Try = - mediaTypeRetriever?.retrieve( - hints = MediaTypeHints(fileExtension = url.extension), - blob = this - )?.tryRecover { error -> - when (error) { - is MediaTypeSnifferError.Read -> - Try.failure(error.cause) - MediaTypeSnifferError.NotRecognized -> - Try.success(MediaType.BINARY) - } - } ?: Try.success(MediaType.BINARY) - override suspend fun properties(): Try = Try.success( Resource.Properties { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/BlobChannel.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ReadableChannel.kt similarity index 88% rename from readium/shared/src/main/java/org/readium/r2/shared/util/zip/BlobChannel.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/zip/ReadableChannel.kt index ab3e755816..6941acc6e8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/BlobChannel.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ReadableChannel.kt @@ -14,15 +14,15 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.zip.jvm.ClosedChannelException import org.readium.r2.shared.util.zip.jvm.NonWritableChannelException import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel -internal class BlobChannel( - private val blob: Blob, +internal class ReadableChannel( + private val readable: Readable, private val wrapError: (ReadError) -> IOException ) : SeekableByteChannel { @@ -41,7 +41,7 @@ internal class BlobChannel( } isClosed = true - coroutineScope.launch { blob.close() } + coroutineScope.launch { readable.close() } } override fun isOpen(): Boolean { @@ -55,7 +55,7 @@ internal class BlobChannel( } withContext(Dispatchers.IO) { - val size = blob.length() + val size = readable.length() .mapFailure(wrapError) .getOrThrow() @@ -66,7 +66,7 @@ internal class BlobChannel( val available = size - position val toBeRead = dst.remaining().coerceAtMost(available.toInt()) check(toBeRead > 0) - val bytes = blob.read(position until position + toBeRead) + val bytes = readable.read(position until position + toBeRead) .mapFailure(wrapError) .getOrThrow() check(bytes.size == toBeRead) @@ -99,7 +99,7 @@ internal class BlobChannel( throw ClosedChannelException() } - return runBlocking { blob.length() } + return runBlocking { readable.length() } .mapFailure { wrapError(it) } .getOrThrow() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 9d4a36f404..56f8c1732c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -10,19 +10,17 @@ import java.io.File import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.extensions.unwrapInstance import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.ArchiveFactory -import org.readium.r2.shared.util.resource.BlobMediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toUrl @@ -33,14 +31,11 @@ import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel * An [ArchiveFactory] able to open a ZIP archive served through a stream (e.g. HTTP server, * content URI, etc.). */ -@OptIn(DelicateReadiumApi::class) -internal class StreamingZipArchiveProvider( - private val mediaTypeRetriever: BlobMediaTypeRetriever? = null -) { +internal class StreamingZipArchiveProvider { - suspend fun sniffBlob(blob: Blob): Try { + suspend fun sniffBlob(readable: Readable): Try { return try { - openBlob(blob, ::ReadException, null) + openBlob(readable, ::ReadException, null) Try.success(MediaType.ZIP) } catch (exception: Exception) { when (val e = exception.unwrapInstance(ReadException::class.java)) { @@ -54,7 +49,7 @@ internal class StreamingZipArchiveProvider( suspend fun create( mediaType: MediaType, - blob: Blob + readable: Readable ): Try, ArchiveFactory.Error> { if (mediaType != MediaType.ZIP) { return Try.failure( @@ -66,9 +61,9 @@ internal class StreamingZipArchiveProvider( return try { val container = openBlob( - blob, + readable, ::ReadException, - blob.source + (readable as? Resource)?.source ) Try.success(container) } catch (exception: Exception) { @@ -82,20 +77,20 @@ internal class StreamingZipArchiveProvider( } private suspend fun openBlob( - blob: Blob, + readable: Readable, wrapError: (ReadError) -> IOException, sourceUrl: AbsoluteUrl? ): Container = withContext(Dispatchers.IO) { - val datasourceChannel = BlobChannel(blob, wrapError) + val datasourceChannel = ReadableChannel(readable, wrapError) val channel = wrapBaseChannel(datasourceChannel) val zipFile = ZipFile(channel, true) - StreamingZipContainer(zipFile, sourceUrl, mediaTypeRetriever) + StreamingZipContainer(zipFile, sourceUrl) } internal suspend fun openFile(file: File): ResourceContainer = withContext(Dispatchers.IO) { val fileChannel = FileChannelAdapter(file, "r") val channel = wrapBaseChannel(fileChannel) - StreamingZipContainer(ZipFile(channel), file.toUrl(), mediaTypeRetriever) + StreamingZipContainer(ZipFile(channel), file.toUrl()) } private fun wrapBaseChannel(channel: SeekableByteChannel): SeekableByteChannel { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt index 5191fc1be7..8e33220c94 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt @@ -22,23 +22,18 @@ import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.ArchiveProperties -import org.readium.r2.shared.util.resource.BlobMediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.archive -import org.readium.r2.shared.util.tryRecover +import org.readium.r2.shared.util.resource.filename import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipArchiveEntry import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile @OptIn(DelicateReadiumApi::class) internal class StreamingZipContainer( private val zipFile: ZipFile, - override val source: AbsoluteUrl?, - private val mediaTypeRetriever: BlobMediaTypeRetriever? + override val source: AbsoluteUrl? ) : Container { private inner class Entry( @@ -51,6 +46,7 @@ internal class StreamingZipContainer( override suspend fun properties(): ResourceTry = Try.success( Resource.Properties { + filename = url.filename archive = ArchiveProperties( entryLength = compressedLength ?: length().getOrElse { return Try.failure(it) }, @@ -59,19 +55,6 @@ internal class StreamingZipContainer( } ) - override suspend fun mediaType(): ResourceTry = - mediaTypeRetriever?.retrieve( - hints = MediaTypeHints(fileExtension = url.extension), - blob = this - )?.tryRecover { error -> - when (error) { - is MediaTypeSnifferError.Read -> - Try.failure(error.cause) - MediaTypeSnifferError.NotRecognized -> - Try.success(MediaType.BINARY) - } - } ?: Try.success(MediaType.BINARY) - override suspend fun length(): ResourceTry = entry.size.takeUnless { it == -1L } ?.let { Try.success(it) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt index 7ba29253dc..56cb7a6bac 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt @@ -6,32 +6,24 @@ package org.readium.r2.shared.util.zip -import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Blob import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeSniffer import org.readium.r2.shared.util.resource.ArchiveFactory -import org.readium.r2.shared.util.resource.BlobMediaTypeRetriever import org.readium.r2.shared.util.resource.Resource -@OptIn(DelicateReadiumApi::class) -public class ZipArchiveFactory( - mediaTypeSniffer: MediaTypeSniffer -) : ArchiveFactory { +public class ZipArchiveFactory : ArchiveFactory { - private val mediaTypeRetriever = BlobMediaTypeRetriever(mediaTypeSniffer, null) + private val fileZipArchiveProvider = FileZipArchiveProvider() - private val fileZipArchiveProvider = FileZipArchiveProvider(mediaTypeRetriever) - - private val streamingZipArchiveProvider = StreamingZipArchiveProvider(mediaTypeRetriever) + private val streamingZipArchiveProvider = StreamingZipArchiveProvider() override suspend fun create( mediaType: MediaType, - blob: Blob + readable: Readable ): Try, ArchiveFactory.Error> = - blob.source?.toFile() - ?.let { fileZipArchiveProvider.create(mediaType, blob) } - ?: streamingZipArchiveProvider.create(mediaType, blob) + (readable as? Resource)?.source?.toFile() + ?.let { fileZipArchiveProvider.create(mediaType, it) } + ?: streamingZipArchiveProvider.create(mediaType, readable) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt index 593babd57b..7c7aca5849 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt @@ -7,11 +7,12 @@ package org.readium.r2.shared.util.zip import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Blob +import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeSniffer import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.Resource public object ZipMediaTypeSniffer : MediaTypeSniffer { @@ -25,10 +26,10 @@ public object ZipMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(blob: Blob): Try { - blob.source?.toFile() - ?.let { return FileZipArchiveProvider().sniffBlob(blob) } + override suspend fun sniffBlob(readable: Readable): Try { + (readable as? Resource)?.source?.toFile() + ?.let { return FileZipArchiveProvider().sniffFile(it) } - return StreamingZipArchiveProvider().sniffBlob(blob) + return StreamingZipArchiveProvider().sniffBlob(readable) } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt index 9d79c07479..3f7fafd821 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt @@ -22,8 +22,8 @@ import org.readium.r2.shared.publication.* import org.readium.r2.shared.readBlocking import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.FileResource import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toAbsoluteUrl import org.robolectric.RobolectricTestRunner @@ -61,7 +61,7 @@ class CoverServiceTest { ), container = ResourceContainer( coverPath, - FileBlob(coverPath.toFile()!!, mediaType = MediaType.JPEG) + FileResource(coverPath.toFile()!!, mediaType = MediaType.JPEG) ) ) } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt index e9f6a805a7..1bf64d0fdc 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt @@ -7,7 +7,6 @@ import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures -import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @@ -123,7 +122,7 @@ class BufferingResourceTest { private val file = Fixtures("util/resource").fileAt("epub.epub") private val data = file.readBytes() - private val resource = FileBlob(file, MediaType.EPUB) + private val resource = FileResource(file, MediaType.EPUB) private fun sut(bufferSize: Long = 1024): BufferingResource = BufferingResource(resource, bufferSize = bufferSize) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt index cf839544de..2e5351bd28 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt @@ -6,7 +6,6 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue import org.junit.Test import org.junit.runner.RunWith -import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @@ -21,7 +20,7 @@ class ResourceInputStreamTest { @Test fun `stream can be read by chunks`() { - val resource = FileBlob(file, mediaType = MediaType.EPUB) + val resource = FileResource(file, mediaType = MediaType.EPUB) val resourceStream = ResourceInputStream(resource) val outputStream = ByteArrayOutputStream(fileContent.size) resourceStream.copyTo(outputStream, bufferSize = bufferSize) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt index 2eb5a51511..fa1c717f4f 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -20,7 +20,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.assertSuccess -import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.use import org.readium.r2.shared.util.zip.FileZipArchiveProvider @@ -42,7 +41,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { assertNotNull( FileZipArchiveProvider(MediaTypeRetriever()) .create( - FileBlob(File(epubZip.path), mediaType = MediaType.EPUB), + FileResource(File(epubZip.path), mediaType = MediaType.EPUB), password = null ) .getOrNull() diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index c25f3ff53a..4029a02aab 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -21,15 +21,13 @@ import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpContainer import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.streamer.parser.PublicationParser import timber.log.Timber internal class ParserAssetFactory( private val httpClient: HttpClient, - private val formatRegistry: FormatRegistry, - private val mediaTypeRetriever: MediaTypeRetriever + private val formatRegistry: FormatRegistry ) { sealed class Error( @@ -118,7 +116,7 @@ internal class ParserAssetFactory( Url("manifest.json")!!, asset.resource ), - HttpContainer(baseUrl, resources, httpClient, mediaTypeRetriever) + HttpContainer(baseUrl, resources, httpClient) ) return Try.success( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index b3069c5c4a..dbb73cd546 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -56,9 +56,9 @@ public class PublicationFactory( ignoreDefaultParsers: Boolean = false, contentProtections: List, formatRegistry: FormatRegistry, - mediaTypeRetriever: MediaTypeRetriever, httpClient: HttpClient, pdfFactory: PdfDocumentFactory<*>?, + private val mediaTypeRetriever: MediaTypeRetriever, private val onCreatePublication: Publication.Builder.() -> Unit = {} ) { public sealed class Error( @@ -90,7 +90,7 @@ public class PublicationFactory( DefaultMediaTypeSniffer() val archiveFactory = - ZipArchiveFactory(mediaTypeSniffer) + ZipArchiveFactory() val formatRegistry = FormatRegistry() @@ -106,8 +106,8 @@ public class PublicationFactory( return PublicationFactory( context = context, contentProtections = contentProtections, - formatRegistry = formatRegistry, mediaTypeRetriever = mediaTypeRetriever, + formatRegistry = formatRegistry, httpClient = DefaultHttpClient(mediaTypeRetriever), pdfFactory = null, onCreatePublication = onCreatePublication @@ -127,15 +127,15 @@ public class PublicationFactory( EpubParser(), pdfFactory?.let { PdfParser(context, it) }, ReadiumWebPubParser(context, pdfFactory), - ImageParser(), - AudioParser() + ImageParser(mediaTypeRetriever), + AudioParser(mediaTypeRetriever) ) private val parsers: List = parsers + if (!ignoreDefaultParsers) defaultParsers else emptyList() private val parserAssetFactory: ParserAssetFactory = - ParserAssetFactory(httpClient, formatRegistry, mediaTypeRetriever) + ParserAssetFactory(httpClient, formatRegistry) /** * Opens a [Publication] from the given asset. diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt deleted file mode 100644 index 75c76ae890..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Link.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.streamer.extensions - -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.use - -internal suspend fun Container.linkForUrl( - url: Url, - mediaType: MediaType? = null -): Link = - Link( - href = url, - mediaType = mediaType ?: get(url)?.use { it.mediaType().getOrNull() } - ) - -internal suspend fun Resource.toLink(url: Url, mediaType: MediaType? = null): Link = - Link( - href = url, - mediaType = mediaType ?: this.mediaType().getOrNull() - ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index 5ce870ed6c..6cdd399188 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -6,6 +6,7 @@ package org.readium.r2.streamer.parser.audio +import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.LocalizedString import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Metadata @@ -14,11 +15,14 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.MediaTypeRetriever +import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs -import org.readium.r2.streamer.extensions.linkForUrl import org.readium.r2.streamer.parser.PublicationParser /** @@ -27,7 +31,9 @@ import org.readium.r2.streamer.parser.PublicationParser * * It can also work for a standalone audio file. */ -public class AudioParser : PublicationParser { +public class AudioParser( + private val mediaTypeRetriever: MediaTypeRetriever +) : PublicationParser { override suspend fun parse( asset: PublicationParser.Asset, @@ -42,7 +48,6 @@ public class AudioParser : PublicationParser { asset.container .filter { zabCanContain(it) } .sortedBy { it.toString() } - .toMutableList() } else { listOfNotNull( asset.container.entries.firstOrNull() @@ -59,12 +64,27 @@ public class AudioParser : PublicationParser { ) } + val readingOrderLinks = readingOrder.map { url -> + val mediaType = asset.container[url]!!.use { resource -> + mediaTypeRetriever.retrieve(resource) + .getOrElse { error -> + when (error) { + MediaTypeSnifferError.NotRecognized -> + null + is MediaTypeSnifferError.Read -> + return Try.failure(PublicationParser.Error.ReadError(error.cause)) + } + } + } + Link(href = url, mediaType = mediaType) + } + val manifest = Manifest( metadata = Metadata( conformsTo = setOf(Publication.Profile.AUDIOBOOK), localizedTitle = asset.container.guessTitle()?.let { LocalizedString(it) } ), - readingOrder = readingOrder.map { asset.container.linkForUrl(it) } + readingOrder = readingOrderLinks ) val publicationBuilder = Publication.Builder( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index c9eeace3d3..35d5d40d4b 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -6,6 +6,7 @@ package org.readium.r2.streamer.parser.image +import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.LocalizedString import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Metadata @@ -14,15 +15,15 @@ import org.readium.r2.shared.publication.services.PerResourcePositionsService import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs -import org.readium.r2.streamer.extensions.linkForUrl import org.readium.r2.streamer.parser.PublicationParser /** @@ -31,7 +32,9 @@ import org.readium.r2.streamer.parser.PublicationParser * * It can also work for a standalone bitmap file. */ -public class ImageParser : PublicationParser { +public class ImageParser( + private val mediaTypeRetriever: MediaTypeRetriever +) : PublicationParser { override suspend fun parse( asset: PublicationParser.Asset, @@ -44,13 +47,11 @@ public class ImageParser : PublicationParser { val readingOrder = if (asset.mediaType.matches(MediaType.CBZ)) { (asset.container) - .filter { !it.isHiddenOrThumbs && entryIsBitmap(asset.container, it) } + .filter { cbzCanContain(it) } .sortedBy { it.toString() } } else { listOfNotNull(asset.container.firstOrNull()) } - .map { asset.container.linkForUrl(it) } - .toMutableList() if (readingOrder.isEmpty()) { return Try.failure( @@ -62,15 +63,30 @@ public class ImageParser : PublicationParser { ) } + val readingOrderLinks = readingOrder.map { url -> + val mediaType = asset.container[url]!!.use { resource -> + mediaTypeRetriever.retrieve(resource) + .getOrElse { error -> + when (error) { + MediaTypeSnifferError.NotRecognized -> + null + is MediaTypeSnifferError.Read -> + return Try.failure(PublicationParser.Error.ReadError(error.cause)) + } + } + } + Link(href = url, mediaType = mediaType) + }.toMutableList() + // First valid resource is the cover. - readingOrder[0] = readingOrder[0].copy(rels = setOf("cover")) + readingOrderLinks[0] = readingOrderLinks[0].copy(rels = setOf("cover")) val manifest = Manifest( metadata = Metadata( conformsTo = setOf(Publication.Profile.DIVINA), localizedTitle = asset.container.guessTitle()?.let { LocalizedString(it) } ), - readingOrder = readingOrder + readingOrder = readingOrderLinks ) val publicationBuilder = Publication.Builder( @@ -86,6 +102,11 @@ public class ImageParser : PublicationParser { return Try.success(publicationBuilder) } - private suspend fun entryIsBitmap(container: Container, url: Url) = - container.get(url)!!.use { it.mediaType() }.getOrNull()?.isBitmap == true + private fun cbzCanContain(url: Url): Boolean = + url.extension?.lowercase() in bitmapExtensions && !url.isHiddenOrThumbs + + private val bitmapExtensions = listOf( + "bmp", "dib", "gif", "jif", "jfi", "jfif", "jpg", "jpeg", + "png", "tif", "tiff", "webp" + ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index 32de55825f..b81283fce8 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -20,7 +20,6 @@ import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.pdf.PdfDocumentFactory import org.readium.r2.shared.util.pdf.toLinks -import org.readium.r2.streamer.extensions.toLink import org.readium.r2.streamer.parser.PublicationParser /** @@ -68,7 +67,7 @@ public class PdfParser( readingProgression = document.readingProgression, numberOfPages = document.pageCount ), - readingOrder = listOf(resource.toLink(url, MediaType.PDF)), + readingOrder = listOf(Link(href = url, mediaType = MediaType.PDF)), tableOfContents = tableOfContents ) diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index b00b994484..2a2234fa63 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -19,8 +19,8 @@ import org.junit.runner.RunWith import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.firstWithRel import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.FileBlob import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.FileResource import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toUrl @@ -36,7 +36,7 @@ class ImageParserTest { private val cbzAsset = runBlocking { val file = fileForResource("futuristic_tales.cbz") - val resource = FileBlob(file, mediaType = MediaType.CBZ) + val resource = FileResource(file, mediaType = MediaType.CBZ) val archive = FileZipArchiveProvider(MediaTypeRetriever()).create( resource, password = null @@ -46,7 +46,7 @@ class ImageParserTest { private val jpgAsset = runBlocking { val file = fileForResource("futuristic_tales.jpg") - val resource = FileBlob(file, mediaType = MediaType.JPEG) + val resource = FileResource(file, mediaType = MediaType.JPEG) PublicationParser.Asset( mediaType = MediaType.JPEG, ResourceContainer(file.toUrl(), resource) diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 3cbf6869aa..4258b10c0d 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -39,7 +39,7 @@ class Readium(context: Context) { DefaultMediaTypeSniffer() private val archiveFactory = - ZipArchiveFactory(mediaTypeSniffer) + ZipArchiveFactory() val formatRegistry = FormatRegistry() @@ -57,9 +57,9 @@ class Readium(context: Context) { ) private val resourceFactory = CompositeResourceFactory( - FileResourceFactory(mediaTypeRetriever), - ContentResourceFactory(context.contentResolver, mediaTypeRetriever), - HttpResourceFactory(httpClient, mediaTypeRetriever) + FileResourceFactory(), + ContentResourceFactory(context.contentResolver), + HttpResourceFactory(httpClient) ) val assetRetriever = AssetRetriever( From 1b167a4741bb84412cf7de2d85c0ce156067756b Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Tue, 28 Nov 2023 12:48:07 +0100 Subject: [PATCH 33/86] Fix most of the tests Remove some uses of MediaTypeRetriever --- .../readium/r2/lcp/LcpPublicationRetriever.kt | 24 ++- .../java/org/readium/r2/lcp/LcpService.kt | 2 +- .../readium/r2/lcp/service/LicensesService.kt | 2 +- .../readium/r2/lcp/service/NetworkService.kt | 15 +- .../r2/navigator/media/ExoMediaPlayer.kt | 2 +- .../media/tts/session/TtsSessionAdapter.kt | 2 +- .../java/org/readium/r2/opds/OPDS1Parser.kt | 17 +- .../java/org/readium/r2/opds/OPDS2Parser.kt | 7 +- .../r2/shared/publication/Publication.kt | 47 +--- .../LcpFallbackContentProtection.kt | 28 +-- .../services/ContentProtectionService.kt | 201 +----------------- .../publication/services/CoverService.kt | 45 +--- .../publication/services/PositionsService.kt | 53 +---- .../readium/r2/shared/util/Benchmarking.kt | 2 - .../readium/r2/shared/util/NetworkError.kt | 40 ---- .../java/org/readium/r2/shared/util/Try.kt | 6 +- .../{resource => archive}/ArchiveFactory.kt | 3 +- .../ArchiveProperties.kt | 3 +- .../SmartArchiveFactory.kt | 5 +- .../r2/shared/util/asset/AssetRetriever.kt | 5 +- .../DefaultMediaTypeSniffer.kt | 28 ++- .../{resource => asset}/MediaTypeRetriever.kt | 83 ++------ .../asset/SimpleResourceMediaTypeRetriever.kt | 83 ++++++++ .../readium/r2/shared/util/data/Container.kt | 3 + .../shared/util/data/ReadableInputStream.kt | 2 +- .../android/AndroidDownloadManager.kt | 2 +- .../r2/shared/util/http/DefaultHttpClient.kt | 43 +--- .../readium/r2/shared/util/http/HttpError.kt | 4 +- .../shared/util/mediatype/FormatRegistry.kt | 8 + .../r2/shared/util/mediatype/MediaType.kt | 9 + .../shared/util/mediatype/MediaTypeSniffer.kt | 60 +++++- .../util/resource/BlobMediaTypeRetriever.kt | 102 --------- .../util/resource/DirectoryContainer.kt | 3 +- .../shared/util/zip/FileZipArchiveProvider.kt | 2 +- .../r2/shared/util/zip/FileZipContainer.kt | 7 +- .../util/zip/StreamingZipArchiveProvider.kt | 2 +- .../shared/util/zip/StreamingZipContainer.kt | 9 +- .../r2/shared/util/zip/ZipArchiveFactory.kt | 2 +- .../r2/shared/publication/PublicationTest.kt | 30 +-- .../AdeptFallbackContentProtectionTest.kt | 16 +- .../LcpFallbackContentProtectionTest.kt | 19 +- .../publication/protection/TestContainer.kt | 50 +---- .../publication/services/CoverServiceTest.kt | 18 +- .../services/PositionsServiceTest.kt | 69 ------ .../HtmlResourceContentIteratorTest.kt | 5 +- .../util/mediatype/FormatRegistryTest.kt | 11 +- .../util/mediatype/MediaTypeRetrieverTest.kt | 134 ++++++------ .../util/resource/BufferingResourceTest.kt | 5 +- .../util/resource/DirectoryContainerTest.kt | 66 +++--- .../r2/shared/util/resource/PropertiesTest.kt | 12 +- .../util/resource/ResourceInputStreamTest.kt | 3 +- .../shared/util/resource/ZipContainerTest.kt | 60 +++--- readium/streamer/build.gradle.kts | 1 + .../readium/r2/streamer/PublicationFactory.kt | 6 +- .../r2/streamer/extensions/Container.kt | 6 +- .../r2/streamer/parser/audio/AudioParser.kt | 4 +- .../parser/epub/EpubPositionsService.kt | 2 +- .../r2/streamer/parser/image/ImageParser.kt | 2 +- .../parser/readium/ReadiumWebPubParser.kt | 25 ++- ...ContainerEntryTest.kt => ContainerTest.kt} | 34 +-- .../parser/epub/EpubDeobfuscatorTest.kt | 51 +++-- .../parser/epub/EpubPositionsServiceTest.kt | 76 +++---- .../parser/epub/PackageDocumentTest.kt | 5 +- .../streamer/parser/image/ImageParserTest.kt | 26 ++- .../java/org/readium/r2/testapp/Readium.kt | 8 +- .../r2/testapp/domain/PublicationUserError.kt | 2 +- .../r2/testapp/domain/ReadUserError.kt | 2 +- 67 files changed, 617 insertions(+), 1092 deletions(-) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/NetworkError.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource => archive}/ArchiveFactory.kt (95%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource => archive}/ArchiveProperties.kt (95%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource => archive}/SmartArchiveFactory.kt (88%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{mediatype => asset}/DefaultMediaTypeSniffer.kt (50%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource => asset}/MediaTypeRetriever.kt (58%) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobMediaTypeRetriever.kt rename readium/streamer/src/test/java/org/readium/r2/streamer/extensions/{ContainerEntryTest.kt => ContainerTest.kt} (51%) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt index eadaee4cac..ee40c601d9 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt @@ -15,11 +15,12 @@ import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.getOrElse 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.resource.MediaTypeRetriever /** * Utility to acquire a protected publication from an LCP License Document. @@ -193,18 +194,19 @@ public class LcpPublicationRetriever( } downloadsRepository.removeDownload(requestId.value) - val mt = mediaTypeRetriever.retrieve( + val mediaTypeWithoutLicense = mediaTypeRetriever.retrieve( + download.file, MediaTypeHints( mediaTypes = listOfNotNull( license.publicationLink.mediaType, download.mediaType ) ) - ) ?: MediaType.EPUB + ).getOrElse { MediaType.EPUB } try { // Saves the License Document into the downloaded publication - val container = createLicenseContainer(download.file, mt) + val container = createLicenseContainer(download.file, mediaTypeWithoutLicense) container.write(license) } catch (e: Exception) { tryOrLog { download.file.delete() } @@ -214,10 +216,20 @@ public class LcpPublicationRetriever( return@launch } + val mediaType = mediaTypeRetriever.retrieve( + download.file, + MediaTypeHints( + mediaTypes = listOfNotNull( + license.publicationLink.mediaType, + download.mediaType + ) + ) + ).getOrElse { MediaType.EPUB } + val acquiredPublication = LcpService.AcquiredPublication( localFile = download.file, - suggestedFilename = "${license.id}.${formatRegistry.fileExtension(mt) ?: "epub"}", - mediaType = mt, + suggestedFilename = "${license.id}.${formatRegistry.fileExtension(mediaType) ?: "epub"}", + mediaType = mediaType, licenseDocument = license ) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt index 877753e172..655f97b444 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt @@ -30,9 +30,9 @@ import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.MediaTypeRetriever /** * Service used to acquire and open publications protected with LCP. diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index 9c5c19965b..8da051b4d0 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -37,11 +37,11 @@ import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.MediaTypeRetriever import timber.log.Timber internal class LicensesService( diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt index 698c9ee654..31808ea74b 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt @@ -23,10 +23,13 @@ import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.MediaTypeRetriever +import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.getOrElse 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.resource.MediaTypeRetriever +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import timber.log.Timber internal typealias URLParameters = Map @@ -140,8 +143,16 @@ internal class NetworkService( } mediaTypeRetriever.retrieve( + destination, MediaTypeHints(connection, mediaType = mediaType.toString()) - ) + ).getOrElse { + when (it) { + is MediaTypeSnifferError.NotRecognized -> + MediaType.BINARY + is MediaTypeSnifferError.Read -> + throw ReadException(it.cause) + } + } } catch (e: Exception) { Timber.e(e) throw LcpException(LcpError.Network(e)) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt index a1688c40a8..c8e32d841f 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt @@ -205,7 +205,7 @@ public class ExoMediaPlayer( var resourceError: ReadError? = error.asInstance() if (resourceError == null && (error.cause as? HttpDataSource.HttpDataSourceException)?.cause is UnknownHostException) { resourceError = ReadError.Access( - HttpError.UnreachableHost(ThrowableError(error.cause!!)) + HttpError.Unreachable(ThrowableError(error.cause!!)) ) } diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt index 59f54f6f6c..dd9b34e685 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/session/TtsSessionAdapter.kt @@ -931,7 +931,7 @@ internal class TtsSessionAdapter( ERROR_CODE_IO_BAD_HTTP_STATUS is HttpError.Timeout -> ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT - is HttpError.UnreachableHost -> + is HttpError.Unreachable -> ERROR_CODE_IO_NETWORK_CONNECTION_FAILED else -> ERROR_CODE_UNSPECIFIED } diff --git a/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt b/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt index f84b408525..3660d73264 100644 --- a/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt +++ b/readium/opds/src/main/java/org/readium/r2/opds/OPDS1Parser.kt @@ -24,7 +24,6 @@ import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.fetchWithDecoder import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.xml.ElementNode import org.readium.r2.shared.util.xml.XmlParser @@ -51,7 +50,7 @@ public class OPDS1Parser { public suspend fun parseUrlString( url: String, - client: HttpClient = DefaultHttpClient(MediaTypeRetriever()) + client: HttpClient = DefaultHttpClient() ): Try = AbsoluteUrl(url) ?.let { parseRequest(HttpRequest(it), client) } @@ -59,7 +58,7 @@ public class OPDS1Parser { public suspend fun parseRequest( request: HttpRequest, - client: HttpClient = DefaultHttpClient(MediaTypeRetriever()) + client: HttpClient = DefaultHttpClient() ): Try { return client.fetchWithDecoder(request) { this.parse(it.body, request.url) @@ -133,9 +132,7 @@ public class OPDS1Parser { val newLink = Link( href = feed.href.resolve(href), - mediaType = mediaTypeRetriever.retrieve( - mediaType = link.getAttr("type") - ), + mediaType = link.getAttr("type")?.let { MediaType(it) }, title = entry.getFirst("title", Namespaces.Atom)?.text, rels = listOfNotNull(link.getAttr("rel")).toSet(), properties = Properties(otherProperties = otherProperties) @@ -154,7 +151,7 @@ public class OPDS1Parser { val hrefAttr = link.getAttr("href")?.let { Url(it) } ?: continue val href = feed.href.resolve(hrefAttr) val title = link.getAttr("title") - val type = mediaTypeRetriever.retrieve(link.getAttr("type")) + val type = link.getAttr("type")?.let { MediaType(it) } val rels = listOfNotNull(link.getAttr("rel")).toSet() val facetGroupName = link.getAttrNs("facetGroup", Namespaces.Opds) @@ -197,7 +194,7 @@ public class OPDS1Parser { @Suppress("unused") public suspend fun retrieveOpenSearchTemplate( feed: Feed, - client: HttpClient = DefaultHttpClient(MediaTypeRetriever()) + client: HttpClient = DefaultHttpClient() ): Try { var openSearchURL: Href? = null var selfMimeType: MediaType? = null @@ -280,7 +277,7 @@ public class OPDS1Parser { Link( href = baseUrl.resolve(href), - mediaType = mediaTypeRetriever.retrieve(element.getAttr("type")), + mediaType = element.getAttr("type")?.let { MediaType(it) }, title = element.getAttr("title"), rels = listOfNotNull(rel).toSet(), properties = Properties(otherProperties = properties) @@ -428,7 +425,5 @@ public class OPDS1Parser { children = fromXML(child) ) } - - public var mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever() } } diff --git a/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt b/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt index 7d2f5c5524..5d1c35e0e2 100644 --- a/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt +++ b/readium/opds/src/main/java/org/readium/r2/opds/OPDS2Parser.kt @@ -32,7 +32,6 @@ import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.fetchWithDecoder -import org.readium.r2.shared.util.resource.MediaTypeRetriever public enum class OPDS2ParserError { MetadataNotFound, @@ -48,7 +47,7 @@ public class OPDS2Parser { public suspend fun parseUrlString( url: String, - client: HttpClient = DefaultHttpClient(MediaTypeRetriever()) + client: HttpClient = DefaultHttpClient() ): Try = AbsoluteUrl(url) ?.let { parseRequest(HttpRequest(it), client) } @@ -56,7 +55,7 @@ public class OPDS2Parser { public suspend fun parseRequest( request: HttpRequest, - client: HttpClient = DefaultHttpClient(MediaTypeRetriever()) + client: HttpClient = DefaultHttpClient() ): Try { return client.fetchWithDecoder(request) { this.parse(it.body, request.url) @@ -283,7 +282,5 @@ public class OPDS2Parser { private fun parseLink(json: JSONObject, baseUrl: Url): Link? = Link.fromJSON(json) ?.normalizeHrefsToBase(baseUrl) - - public var mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever() } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index c68dd49189..57fd269799 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -30,14 +30,10 @@ import org.readium.r2.shared.publication.services.WebPositionsService import org.readium.r2.shared.publication.services.content.ContentService import org.readium.r2.shared.publication.services.search.SearchService import org.readium.r2.shared.util.Closeable -import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.EmptyContainer import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpError -import org.readium.r2.shared.util.http.HttpRequest -import org.readium.r2.shared.util.http.HttpStreamResponse import org.readium.r2.shared.util.resource.Resource internal typealias ServiceFactory = (Publication.Service.Context) -> Publication.Service? @@ -68,7 +64,7 @@ public class Publication( public val manifest: Manifest, private val container: PublicationContainer = EmptyContainer(), private val servicesBuilder: ServicesBuilder = ServicesBuilder(), - private val httpClient: HttpClient? = null, + httpClient: HttpClient? = null, @Deprecated( "Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR @@ -357,45 +353,6 @@ public class Publication( override fun close() {} } - public interface WebService : Service { - - /** - * Links which will be added to [Publication.links]. - * It can be used to expose a web API for the service, through [Publication.get]. - * - * To disambiguate the href with a publication's local resources, you should use the prefix - * `/~readium/`. A custom media type or rel should be used to identify the service. - * - * You can use a templated URI to accept query parameters, e.g.: - * - * ``` - * Link( - * href = "/~readium/search{?text}", - * type = "application/vnd.readium.search+json", - * templated = true - * ) - * ``` - */ - public val links: List - - /** - * A service can return a Resource to: - * - respond to a request to its web API declared in links, - * - serve additional resources on behalf of the publication, - * - replace a publication resource by its own version. - * - * Called by [Publication.get] for each request. - * - * Warning: If you need to request one of the publication resources to answer the request, - * use the [Container] provided by the [Publication.Service.Context] instead of - * [Publication.get], otherwise it will trigger an infinite loop. - * - * @return The [Resource] containing the response, or null if the service doesn't - * recognize this request. - */ - public suspend fun handle(request: HttpRequest): Try? - } - /** * Builds a list of [Publication.Service] from a collection of service factories. * @@ -428,7 +385,7 @@ public class Publication( ) /** Builds the actual list of publication services to use in a Publication. */ - public fun build(context: Service.Context, httpClient: HttpClient?): List { + public fun build(context: Service.Context, httpClient: HttpClient? = null): List { val serviceFactories = buildMap { putAll(this@ServicesBuilder.serviceFactories) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index 6ae06a687c..63f3874bca 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -16,7 +16,6 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.data.DecoderError import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.readAsJson import org.readium.r2.shared.util.data.readAsRwpm import org.readium.r2.shared.util.data.readAsXml import org.readium.r2.shared.util.getOrElse @@ -71,35 +70,28 @@ public class LcpFallbackContentProtection : ContentProtection { } private suspend fun isLcpProtected(container: ResourceContainer, mediaType: MediaType): Try { - val isReadiumWebpub = mediaType.matches(MediaType.READIUM_WEBPUB) || - mediaType.matches(MediaType.LCP_PROTECTED_PDF) || - mediaType.matches(MediaType.LCP_PROTECTED_AUDIOBOOK) - + val isRpf = mediaType.isRpf val isEpub = mediaType.matches(MediaType.EPUB) - if (!isReadiumWebpub && !isEpub) { + if (!isRpf && !isEpub) { return Try.success(false) } - container.get(Url("license.lcpl")!!) - ?.readAsJson() - ?.getOrElse { - when (it) { - is DecoderError.Read -> - Try.failure(it.cause.cause) - is DecoderError.Decoding -> - return Try.success(false) - } - } + val licenseUrl = when { + isRpf -> Url("license.lcpl")!! + else -> Url("META-INF/license.lcpl")!! // isEpub + } + container[licenseUrl] + ?.let { return Try.success(true) } return when { - isReadiumWebpub -> hasLcpSchemeInManifest(container) + isRpf -> hasLcpSchemeInManifest(container) else -> hasLcpSchemeInEncryptionXml(container) // isEpub } } private suspend fun hasLcpSchemeInManifest(container: ResourceContainer): Try { - val manifest = container.get(Url("manifest.json")!!) + val manifest = container[Url("manifest.json")!!] ?.readAsRwpm() ?.getOrElse { when (it) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt index 0325ff2466..18acc14255 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/ContentProtectionService.kt @@ -9,29 +9,17 @@ package org.readium.r2.shared.publication.services -import java.util.Locale -import org.json.JSONObject -import org.readium.r2.shared.publication.Href -import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.LocalizedString import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationServicesHolder import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.http.HttpError -import org.readium.r2.shared.util.http.HttpRequest -import org.readium.r2.shared.util.http.HttpResponse -import org.readium.r2.shared.util.http.HttpStatus -import org.readium.r2.shared.util.http.HttpStreamResponse -import org.readium.r2.shared.util.mediatype.MediaType /** * Provides information about a publication's content protection and manages user rights. */ -public interface ContentProtectionService : Publication.WebService { +public interface ContentProtectionService : Publication.Service { /** * Whether the [Publication] has a restricted access to its resources, and can't be rendered in @@ -65,14 +53,6 @@ public interface ContentProtectionService : Publication.WebService { */ public val name: String? get() = null - override val links: List - get() = RouteHandler.links - - override suspend fun handle(request: HttpRequest): Try? { - val route = RouteHandler.route(request.url) ?: return null - return route.handleRequest(request, this) - } - /** * Manages consumption of user rights and permissions. */ @@ -238,182 +218,3 @@ public val Publication.protectionLocalizedName: LocalizedString */ public val Publication.protectionName: String? get() = protectionService?.name - -private sealed class RouteHandler { - - companion object { - - private val handlers = listOf( - ContentProtectionHandler, - RightsCopyHandler, - RightsPrintHandler - ) - - val links = handlers.map { it.link } - - fun route(url: Url): RouteHandler? = handlers.firstOrNull { it.acceptRequest(url) } - } - - abstract val link: Link - - abstract fun acceptRequest(url: Url): Boolean - - abstract suspend fun handleRequest(request: HttpRequest, service: ContentProtectionService): Try - - object ContentProtectionHandler : RouteHandler() { - - private val path = "/~readium/content-protection" - private val mediaType = MediaType("application/vnd.readium.content-protection+json")!! - - override val link = Link( - href = Url(path)!!, - mediaType = mediaType - ) - - override fun acceptRequest(url: Url): Boolean = - url.path == path - - override suspend fun handleRequest(request: HttpRequest, service: ContentProtectionService): Try { - val json = JSONObject().apply { - put("isRestricted", service.isRestricted) - putOpt("error", service.error?.message) - putOpt("name", service.name) - put("rights", service.rights.toJSON()) - } - - val response = HttpResponse( - request = request, - url = request.url, - 200, - emptyMap(), - mediaType - ) - - val body = json - .toString() - .byteInputStream(charset = Charsets.UTF_8) - - return Try.success( - HttpStreamResponse(response, body) - ) - } - } - - object RightsCopyHandler : RouteHandler() { - - private val mediaType = MediaType("application/vnd.readium.rights.copy+json")!! - private val path = "/~readium/rights/copy" - - override val link: Link = Link( - href = Href("$path{?text,peek}", templated = true)!!, - mediaType = mediaType - ) - - override fun acceptRequest(url: Url): Boolean = - url.path == path - - override suspend fun handleRequest(request: HttpRequest, service: ContentProtectionService): Try { - val query = request.url.query - val text = query.firstNamedOrNull("text") - ?: return Try.failure( - badRequestResponse("'text' parameter is required.") - ) - val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() - ?: return Try.failure( - badRequestResponse("If present, 'peek' must be true or false.") - ) - - val copyAllowed = with(service.rights) { if (peek) canCopy(text) else copy(text) } - - return if (!copyAllowed) { - Try.failure(forbiddenResponse()) - } else { - Try.success(trueResponse(request)) - } - } - } - - object RightsPrintHandler : RouteHandler() { - - private val mediaType = MediaType("application/vnd.readium.rights.print+json")!! - private val path = "/~readium/rights/print" - - override val link = Link( - href = Href("$path{?pageCount,peek}", templated = true)!!, - mediaType = mediaType - ) - - override fun acceptRequest(url: Url): Boolean = - url.path == path - - override suspend fun handleRequest(request: HttpRequest, service: ContentProtectionService): Try { - val query = request.url.query - val pageCountString = query.firstNamedOrNull("pageCount") - ?: return Try.failure( - badRequestResponse("'pageCount' parameter is required") - ) - - val pageCount = pageCountString.toIntOrNull()?.takeIf { it >= 0 } - ?: return Try.failure( - badRequestResponse("'pageCount' must be a positive integer") - ) - val peek = (query.firstNamedOrNull("peek") ?: "false").toBooleanOrNull() - ?: return Try.failure( - badRequestResponse("If present, 'peek' must be true or false") - ) - - val printAllowed = with(service.rights) { - if (peek) { - canPrint(pageCount) - } else { - print( - pageCount - ) - } - } - - return if (!printAllowed) { - Try.failure(forbiddenResponse()) - } else { - Try.success(trueResponse(request)) - } - } - } - - fun String.toBooleanOrNull(): Boolean? = when (this.lowercase(Locale.getDefault())) { - "true" -> true - "false" -> false - else -> null - } - - fun ContentProtectionService.UserRights.toJSON() = JSONObject().apply { - put("canCopy", canCopy) - put("canPrint", canPrint) - } -} - -private fun trueResponse(request: HttpRequest): HttpStreamResponse = - HttpStreamResponse( - response = HttpResponse( - request, - request.url, - 200, - emptyMap(), - MediaType.JSON - ), - body = "true".byteInputStream() - ) -private fun forbiddenResponse(): HttpError.Response = - HttpError.Response( - HttpStatus.Forbidden - ) - -private fun badRequestResponse(detail: String): HttpError.Response = - HttpError.Response( - HttpStatus.BadRequest, - MediaType.JSON_PROBLEM_DETAILS, - JSONObject().apply { - put("title", "Bad request") - put("detail", detail) - }.toString().encodeToByteArray() - ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index d312b9d4ed..b39a3d0bf8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -13,25 +13,18 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.util.Size import org.readium.r2.shared.extensions.scaleToFit -import org.readium.r2.shared.extensions.toPng import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.publication.firstWithRel 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.data.Container import org.readium.r2.shared.util.data.readAsBitmap import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest -import org.readium.r2.shared.util.http.HttpResponse -import org.readium.r2.shared.util.http.HttpStatus -import org.readium.r2.shared.util.http.HttpStreamResponse import org.readium.r2.shared.util.http.fetch -import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource /** @@ -131,7 +124,7 @@ internal class ResourceCoverService( ) : CoverService { override suspend fun cover(): Bitmap? { - val resource = container.get(coverUrl) + val resource = container[coverUrl] ?: return null return resource.readAsBitmap() @@ -155,42 +148,8 @@ internal class ResourceCoverService( /** * A [CoverService] which provides a unique cover for each Publication. */ -public abstract class GeneratedCoverService : CoverService, Publication.WebService { - - private val coverLink = Link( - href = Url("/~readium/cover")!!, - mediaType = MediaType.PNG, - rels = setOf("cover") - ) - - override val links: List = listOf(coverLink) - +public abstract class GeneratedCoverService : CoverService { abstract override suspend fun cover(): Bitmap - - override suspend fun handle(request: HttpRequest): Try? { - if (request.url != coverLink.url()) { - return null - } - - val cover = cover() - val png = cover.toPng() - ?: return Try.failure( - HttpError.Response( - HttpStatus(500), - null, - null - ) - ) - - val response = HttpResponse(request, request.url, 200, emptyMap(), MediaType.PNG) - - return Try.success( - HttpStreamResponse( - response = response, - body = png.inputStream() - ) - ) - } } /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt index dd369ec297..69c074f31d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt @@ -21,29 +21,19 @@ import org.readium.r2.shared.publication.PublicationServicesHolder import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.publication.firstWithMediaType import org.readium.r2.shared.publication.firstWithRel -import org.readium.r2.shared.toJSON 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.http.HttpClient -import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest -import org.readium.r2.shared.util.http.HttpResponse -import org.readium.r2.shared.util.http.HttpStreamResponse import org.readium.r2.shared.util.mediatype.MediaType private val positionsMediaType = MediaType("application/vnd.readium.position-list+json")!! -private val positionsLink = Link( - href = Url("/~readium/positions")!!, - mediaType = positionsMediaType -) - /** * Provides a list of discrete locations in the publication, no matter what the original format is. */ -public interface PositionsService : Publication.Service, Publication.WebService { +public interface PositionsService : Publication.Service { /** * Returns the list of all the positions in the publication, grouped by the resource reading order index. @@ -54,46 +44,23 @@ public interface PositionsService : Publication.Service, Publication.WebService * Returns the list of all the positions in the publication. */ public suspend fun positions(): List = positionsByReadingOrder().flatten() - - override val links: List get() = listOf(positionsLink) - - override suspend fun handle(request: HttpRequest): Try? { - if (request.url != positionsLink.url()) { - return null - } - - val positions = positions() - - val jsonResponse = JSONObject().apply { - put("total", positions.size) - put("positions", positions.toJSON()) - } - - val stream = jsonResponse - .toString() - .byteInputStream(charset = Charsets.UTF_8) - - val response = HttpResponse(request, request.url, 200, emptyMap(), positionsMediaType) - - return Try.success(HttpStreamResponse(response, stream)) - } } /** * Returns the list of all the positions in the publication, grouped by the resource reading order index. */ -public suspend fun PublicationServicesHolder.positionsByReadingOrder(): List> { - checkNotNull(findService(PositionsService::class)) { "No position service found." } - .let { return it.positionsByReadingOrder() } -} +public suspend fun PublicationServicesHolder.positionsByReadingOrder(): List> = + findService(PositionsService::class) + ?.positionsByReadingOrder() + .orEmpty() /** * Returns the list of all the positions in the publication. */ -public suspend fun PublicationServicesHolder.positions(): List { - checkNotNull(findService(PositionsService::class)) { "No position service found." } - .let { return it.positions() } -} +public suspend fun PublicationServicesHolder.positions(): List = + findService(PositionsService::class) + ?.positions() + .orEmpty() /** * List of all the positions in each resource, indexed by their href. @@ -159,7 +126,7 @@ internal class WebPositionsService( private lateinit var _positions: List - override val links: List = + private val links: List = listOfNotNull( manifest.links.firstWithMediaType(positionsMediaType) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Benchmarking.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Benchmarking.kt index 63775f2d4e..f9f20d4554 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Benchmarking.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Benchmarking.kt @@ -6,11 +6,9 @@ package org.readium.r2.shared.util -import kotlin.time.ExperimentalTime import kotlin.time.measureTime import timber.log.Timber -@OptIn(ExperimentalTime::class) internal inline fun benchmark(title: String, enabled: Boolean = true, closure: () -> T): T { if (!enabled) { return closure() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/NetworkError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/NetworkError.kt deleted file mode 100644 index a659940367..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/NetworkError.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util - -public sealed class NetworkError( - public override val message: String, - public override val cause: Error? = null -) : Error { - - /** Equivalent to a 400 HTTP error. */ - public class BadRequest(cause: Error? = null) : - NetworkError("Invalid request which can't be processed", cause) { - - public constructor(message: String) : this(MessageError(message)) - - public constructor(exception: Exception) : this(ThrowableError(exception)) - } - - /** - * Equivalent to a 503 HTTP error. - * - * Used when the source can't be reached, e.g. no Internet connection, or an issue with the - * file system. Usually this is a temporary error. - */ - public class Unavailable(cause: Error? = null) : - NetworkError("The resource is currently unavailable, please try again later.", cause) { - - public constructor(exception: Exception) : this(ThrowableError(exception)) - } - - /** - * The Internet connection appears to be offline. - */ - public class Offline(cause: Error? = null) : - NetworkError("The Internet connection appears to be offline.", cause) -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt index a7be7aaf93..19981caa4a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt @@ -6,6 +6,9 @@ package org.readium.r2.shared.util +import org.readium.r2.shared.util.Try.Failure +import org.readium.r2.shared.util.Try.Success + /** A [Result] type which can be used as a return type. */ public sealed class Try { @@ -153,9 +156,10 @@ public fun Try.assertSuccess(): S = when (this) { is Try.Success -> value - is Try.Failure -> + is Try.Failure -> { throw IllegalStateException( "Try was excepted to contain a success.", value as? Throwable ?: (value as? Error)?.let { ErrorException(it) } ) + } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt similarity index 95% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt index c30af9d452..51c7b1a747 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt @@ -4,13 +4,14 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.archive import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource /** * A factory to create a [ResourceContainer]s from archive [Readable]s. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProperties.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt similarity index 95% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProperties.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt index 677c2c1129..a1920221bf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProperties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt @@ -4,13 +4,14 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.archive import org.json.JSONObject import org.readium.r2.shared.JSONable import org.readium.r2.shared.extensions.optNullableBoolean import org.readium.r2.shared.extensions.optNullableLong import org.readium.r2.shared.extensions.toMap +import org.readium.r2.shared.util.resource.Resource /** * Holds information about how the resource is stored in the archive. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt similarity index 88% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt index 86f9509e55..ce4cb555e0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SmartArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt @@ -4,13 +4,14 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.archive import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.tryRecover internal class SmartArchiveFactory( @@ -27,7 +28,7 @@ internal class SmartArchiveFactory( when (error) { is ArchiveFactory.Error.FormatNotSupported -> { formatRegistry.superType(mediaType) - ?.let { archiveFactory.create(it, readable) } + ?.let { create(it, readable) } ?: Try.failure(error) } is ArchiveFactory.Error.ReadError -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 42b763f7f9..2960efd583 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -11,13 +11,12 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.archive.ArchiveFactory +import org.readium.r2.shared.util.archive.SmartArchiveFactory import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ArchiveFactory -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.SmartArchiveFactory import org.readium.r2.shared.util.toUrl /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/DefaultMediaTypeSniffer.kt similarity index 50% rename from readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/asset/DefaultMediaTypeSniffer.kt index 015688835a..a45875387c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/DefaultMediaTypeSniffer.kt @@ -4,11 +4,32 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.mediatype +package org.readium.r2.shared.util.asset import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.mediatype.ArchiveMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.BitmapMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.CompositeMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.EpubMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.HtmlMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.JsonMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.LcpLicenseMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.LpfMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeHints +import org.readium.r2.shared.util.mediatype.MediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.mediatype.OpdsMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.PdfMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.RarMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.SystemMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.W3cWpubMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.WebPubManifestMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.WebPubMediaTypeSniffer +import org.readium.r2.shared.util.mediatype.XhtmlMediaTypeSniffer +import org.readium.r2.shared.util.zip.ZipMediaTypeSniffer /** * The default composite sniffer provided by Readium for all known formats. @@ -31,7 +52,10 @@ public class DefaultMediaTypeSniffer : MediaTypeSniffer { LcpLicenseMediaTypeSniffer, W3cWpubMediaTypeSniffer, WebPubManifestMediaTypeSniffer, - JsonMediaTypeSniffer + JsonMediaTypeSniffer, + SystemMediaTypeSniffer, + ZipMediaTypeSniffer, + RarMediaTypeSniffer ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt similarity index 58% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt index fe65de63cf..ee634b01ea 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt @@ -4,24 +4,24 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.asset import android.content.ContentResolver import java.io.File -import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.archive.ArchiveFactory +import org.readium.r2.shared.util.archive.SmartArchiveFactory import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer 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.MediaTypeSniffer import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.mediatype.SystemMediaTypeSniffer +import org.readium.r2.shared.util.resource.FileResource +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use -import org.readium.r2.shared.util.zip.ZipArchiveFactory /** * Retrieves a canonical [MediaType] for the provided media type and file extension hints and/or @@ -30,7 +30,6 @@ import org.readium.r2.shared.util.zip.ZipArchiveFactory * The actual format sniffing is mostly done by the provided [mediaTypeSniffer]. * The [DefaultMediaTypeSniffer] cover the formats supported with Readium by default. */ -@OptIn(DelicateReadiumApi::class) public class MediaTypeRetriever( private val mediaTypeSniffer: MediaTypeSniffer, formatRegistry: FormatRegistry, @@ -38,32 +37,8 @@ public class MediaTypeRetriever( contentResolver: ContentResolver? ) { - public companion object { - - @Deprecated("This overload will be removed without notice as soon as possible.") - public operator fun invoke( - contentResolver: ContentResolver? = null - ): MediaTypeRetriever { - val mediaTypeSniffer = - DefaultMediaTypeSniffer() - - val archiveFactory = - ZipArchiveFactory() - - val formatRegistry = - FormatRegistry() - - return MediaTypeRetriever( - mediaTypeSniffer, - formatRegistry, - archiveFactory, - contentResolver - ) - } - } - - private val blobMediaTypeRetriever: BlobMediaTypeRetriever = - BlobMediaTypeRetriever(mediaTypeSniffer, contentResolver) + private val simpleResourceMediaTypeRetriever: SimpleResourceMediaTypeRetriever = + SimpleResourceMediaTypeRetriever(mediaTypeSniffer, contentResolver, formatRegistry) private val archiveFactory: ArchiveFactory = SmartArchiveFactory(archiveFactory, formatRegistry) @@ -71,25 +46,14 @@ public class MediaTypeRetriever( /** * Retrieves a canonical [MediaType] for the provided media type and file extension [hints]. */ - public fun retrieve(hints: MediaTypeHints): MediaType? { - blobMediaTypeRetriever.retrieve(hints) - ?.let { return it } - - // Falls back on the system-wide registered media types using MimeTypeMap. - // Note: This is done after the default sniffers, because otherwise it will detect - // JSON, XML or ZIP formats before we have a chance of sniffing their content (for example, - // for RWPM). - SystemMediaTypeSniffer.sniffHints(hints) + internal fun retrieve(hints: MediaTypeHints): MediaType? = + simpleResourceMediaTypeRetriever.retrieveUnsafe(hints) .getOrNull() - ?.let { return it } - - return hints.mediaTypes.firstOrNull() - } /** * Retrieves a canonical [MediaType] for the provided [mediaType] and [fileExtension] hints. */ - public fun retrieve(mediaType: String? = null, fileExtension: String? = null): MediaType? = + internal fun retrieve(mediaType: String? = null, fileExtension: String? = null): MediaType? = retrieve( MediaTypeHints( mediaType = mediaType?.let { MediaType(it) }, @@ -100,13 +64,14 @@ public class MediaTypeRetriever( /** * Retrieves a canonical [MediaType] for the provided [mediaType] and [fileExtension] hints. */ - public fun retrieve(mediaType: MediaType, fileExtension: String? = null): MediaType = + + internal fun retrieve(mediaType: MediaType, fileExtension: String? = null): MediaType = retrieve(MediaTypeHints(mediaType = mediaType, fileExtension = fileExtension)) ?: mediaType /** * Retrieves a canonical [MediaType] for the provided [mediaTypes] and [fileExtensions] hints. */ - public fun retrieve( + internal fun retrieve( mediaTypes: List = emptyList(), fileExtensions: List = emptyList() ): MediaType? = @@ -116,9 +81,8 @@ public class MediaTypeRetriever( container: Container, hints: MediaTypeHints = MediaTypeHints() ): Try { - mediaTypeSniffer.sniffHints(hints) - .getOrNull() - ?.let { return Try.success(it) } + simpleResourceMediaTypeRetriever.retrieve(hints) + ?.let { Try.success(it) } mediaTypeSniffer.sniffContainer(container) .onSuccess { return Try.success(it) } @@ -129,9 +93,7 @@ public class MediaTypeRetriever( } } - return hints.mediaTypes.firstOrNull() - ?.let { Try.success(it) } - ?: Try.failure(MediaTypeSnifferError.NotRecognized) + return simpleResourceMediaTypeRetriever.retrieveUnsafe(hints) } public suspend fun retrieve( @@ -147,23 +109,16 @@ public class MediaTypeRetriever( resource: Resource, hints: MediaTypeHints = MediaTypeHints() ): Try { - val properties = resource.properties() - .getOrElse { return Try.failure(MediaTypeSnifferError.Read(it)) } - - mediaTypeSniffer.sniffHints(MediaTypeHints(properties) + hints) - .getOrNull() - ?.let { return Try.success(it) } - - val blobMediaType = blobMediaTypeRetriever.retrieve(hints, resource) + val resourceMediaType = simpleResourceMediaTypeRetriever.retrieve(resource, hints) .getOrElse { return Try.failure(it) } - val container = archiveFactory.create(blobMediaType, resource) + val container = archiveFactory.create(resourceMediaType, resource) .getOrElse { when (it) { is ArchiveFactory.Error.ReadError -> return Try.failure(MediaTypeSnifferError.Read(it.cause)) is ArchiveFactory.Error.FormatNotSupported -> - return Try.success(blobMediaType) + return Try.success(resourceMediaType) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt new file mode 100644 index 0000000000..8721a3feff --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.asset + +import android.content.ContentResolver +import android.provider.MediaStore +import java.io.File +import org.readium.r2.shared.extensions.queryProjection +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse +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.MediaTypeSniffer +import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.invoke +import org.readium.r2.shared.util.toUri +import org.readium.r2.shared.util.tryRecover + +internal class SimpleResourceMediaTypeRetriever( + private val mediaTypeSniffer: MediaTypeSniffer, + private val contentResolver: ContentResolver?, + private val formatRegistry: FormatRegistry +) { + + /** + * Retrieves a canonical [MediaType] for the provided media type and file extension [hints]. + */ + fun retrieve(hints: MediaTypeHints): MediaType? = + retrieveUnsafe(hints) + .getOrNull() + ?.takeUnless { formatRegistry.isSuperType(it) } + + internal fun retrieveUnsafe(hints: MediaTypeHints): Try = + mediaTypeSniffer.sniffHints(hints) + .tryRecover { + hints.mediaTypes.firstOrNull() + ?.let { Try.success(it) } + ?: Try.failure(MediaTypeSnifferError.NotRecognized) + } + + suspend fun retrieve(resource: Resource, hints: MediaTypeHints): Try { + val properties = resource.properties() + .getOrElse { return Try.failure(MediaTypeSnifferError.Read(it)) } + + retrieve(MediaTypeHints(properties) + hints) + ?.also { return Try.success(it) } + + if (contentResolver != null) { + resource.source + ?.takeIf { it.isContent } + ?.let { url -> + val contentHints = MediaTypeHints( + mediaType = contentResolver.getType(url.toUri()) + ?.let { MediaType(it) } + ?.takeUnless { it.matches(MediaType.BINARY) }, + fileExtension = contentResolver + .queryProjection(url.uri, MediaStore.MediaColumns.DISPLAY_NAME) + ?.let { filename -> File(filename).extension } + ) + + retrieve(contentHints) + ?.let { return Try.success(it) } + } + } + + mediaTypeSniffer.sniffBlob(resource) + .onSuccess { return Try.success(it) } + .onFailure { error -> + when (error) { + is MediaTypeSnifferError.NotRecognized -> {} + else -> return Try.failure(error) + } + } + + return retrieveUnsafe(hints) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index f4f6c25cbf..97ba79950d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -9,6 +9,7 @@ package org.readium.r2.shared.util.data import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource /** @@ -16,6 +17,8 @@ import org.readium.r2.shared.util.resource.Resource */ public interface Container : Iterable, SuspendingCloseable { + public val archiveMediaType: MediaType? get() = null + /** * Direct source to this container, when available. */ diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadableInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadableInputStream.kt index c3ff78c4a7..a11710f2aa 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadableInputStream.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadableInputStream.kt @@ -19,7 +19,7 @@ import org.readium.r2.shared.util.Try */ public class ReadableInputStream( private val readable: Readable, - private val wrapError: (ReadError) -> IOException, + private val wrapError: (ReadError) -> IOException = { ReadException(it) }, private val range: LongRange? = null ) : InputStream() { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index 783e4b6eba..78aed0d9a0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.tryOr import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpError @@ -33,7 +34,6 @@ import org.readium.r2.shared.util.http.HttpStatus 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.resource.MediaTypeRetriever import org.readium.r2.shared.util.toUri import org.readium.r2.shared.util.units.Hz import org.readium.r2.shared.util.units.hz diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index beda2ab935..3764547268 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -11,6 +11,7 @@ import java.io.ByteArrayInputStream import java.io.FileInputStream import java.io.IOException import java.io.InputStream +import java.net.ConnectException import java.net.HttpURLConnection import java.net.NoRouteToHostException import java.net.SocketTimeoutException @@ -27,16 +28,8 @@ 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.getOrDefault import org.readium.r2.shared.util.http.HttpRequest.Method import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints -import org.readium.r2.shared.util.resource.InMemoryResource -import org.readium.r2.shared.util.resource.MediaTypeRetriever -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.filename -import org.readium.r2.shared.util.resource.mediaType -import org.readium.r2.shared.util.toAbsoluteUrl import org.readium.r2.shared.util.toDebugDescription import org.readium.r2.shared.util.tryRecover import timber.log.Timber @@ -44,7 +37,6 @@ import timber.log.Timber /** * An implementation of [HttpClient] using the native [HttpURLConnection]. * - * @param mediaTypeRetriever Component used to sniff the media type of the HTTP response. * @param userAgent Custom user agent to use for requests. * @param connectTimeout Timeout used when establishing a connection to the resource. A null timeout * is interpreted as the default value, while a timeout of zero as an infinite timeout. @@ -52,16 +44,15 @@ import timber.log.Timber * as the default value, while a timeout of zero as an infinite timeout. */ public class DefaultHttpClient( - private val mediaTypeRetriever: MediaTypeRetriever, private val userAgent: String? = null, private val connectTimeout: Duration? = null, private val readTimeout: Duration? = null, public var callback: Callback = object : Callback {} ) : HttpClient { - @Suppress("UNUSED_PARAMETER", "DEPRECATION") + @Suppress("UNUSED_PARAMETER") @Deprecated( - "You need to provide a [mediaTypeRetriever]. If you used [additionalHeaders], pass all headers when building your request or modify it in Callback.onStartRequest instead.", + "If you used [additionalHeaders], pass all headers when building your request or modify it in Callback.onStartRequest instead.", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("DefaultHttpClient(mediaTypeRetriever = MediaTypeRetriever())") ) @@ -72,7 +63,6 @@ public class DefaultHttpClient( readTimeout: Duration? = null, callback: Callback = object : Callback {} ) : this( - mediaTypeRetriever = MediaTypeRetriever(), userAgent = userAgent, connectTimeout = connectTimeout, readTimeout = readTimeout, @@ -181,37 +171,18 @@ public class DefaultHttpClient( // JSON Problem Details or OPDS Authentication Document val body = connection.errorStream?.use { it.readBytes() } - val resourceProperties = - Resource.Properties( - Resource.Properties.Builder() - .apply { - mediaType = connection.contentType?.let { MediaType(it) } - filename = connection.url.file - } - - ) - val mediaType = body?.let { - mediaTypeRetriever.retrieve( - InMemoryResource( - it, - connection.url.toAbsoluteUrl(), - resourceProperties - ) - ).getOrDefault(MediaType.BINARY) - } + val mediaType = MediaType(connection.contentType) return@withContext Try.failure( HttpError.Response(HttpStatus(statusCode), mediaType, body) ) } - val mediaType = mediaTypeRetriever.retrieve(MediaTypeHints(connection)) - val response = HttpResponse( request = request, url = request.url, statusCode = statusCode, headers = connection.safeHeaders, - mediaType = mediaType + mediaType = MediaType(connection.contentType) ) callback.onResponseReceived(request, response) @@ -356,8 +327,8 @@ public class DefaultHttpClient( */ private fun wrap(cause: IOException): HttpError = when (cause) { - is UnknownHostException, is NoRouteToHostException -> - HttpError.UnreachableHost(ThrowableError(cause)) + is UnknownHostException, is NoRouteToHostException, is ConnectException -> + HttpError.Unreachable(ThrowableError(cause)) is SocketTimeoutException -> HttpError.Timeout(ThrowableError(cause)) else -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt index 7325a5d9c6..27b4dd4e6a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt @@ -28,8 +28,8 @@ public sealed class HttpError( public class Timeout(cause: Error) : HttpError("Request timed out.", cause) - public class UnreachableHost(cause: Error) : - HttpError("Host could not be reached.", cause) + public class Unreachable(cause: Error) : + HttpError("Server could not be reached.", cause) public class Redirection(cause: Error) : HttpError("Redirection failed.", cause) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt index c34f7f1a31..c44b9678f3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt @@ -12,6 +12,7 @@ package org.readium.r2.shared.util.mediatype public class FormatRegistry( fileExtensions: Map = mapOf( MediaType.ACSM to "acsm", + MediaType.CBR to "cbr", MediaType.CBZ to "cbz", MediaType.DIVINA to "divina", MediaType.DIVINA_MANIFEST to "json", @@ -28,13 +29,20 @@ public class FormatRegistry( MediaType.ZAB to "zab" ), superTypes: Map = mapOf( + MediaType.CBR to MediaType.RAR, MediaType.CBZ to MediaType.ZIP, MediaType.DIVINA to MediaType.READIUM_WEBPUB, MediaType.DIVINA_MANIFEST to MediaType.READIUM_WEBPUB_MANIFEST, MediaType.EPUB to MediaType.ZIP, + MediaType.XHTML to MediaType.XML, + MediaType.JSON_PROBLEM_DETAILS to MediaType.JSON, MediaType.LCP_LICENSE_DOCUMENT to MediaType.JSON, MediaType.LCP_PROTECTED_AUDIOBOOK to MediaType.READIUM_AUDIOBOOK, MediaType.LCP_PROTECTED_PDF to MediaType.READIUM_WEBPUB, + MediaType.OPDS1 to MediaType.XML, + MediaType.OPDS1_ENTRY to MediaType.XML, + MediaType.OPDS2 to MediaType.JSON, + MediaType.OPDS2_PUBLICATION to MediaType.JSON, MediaType.READIUM_AUDIOBOOK to MediaType.READIUM_WEBPUB, MediaType.READIUM_AUDIOBOOK_MANIFEST to MediaType.READIUM_WEBPUB_MANIFEST, MediaType.READIUM_WEBPUB to MediaType.ZIP, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt index 0235a1335a..a7389d6053 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt @@ -197,6 +197,13 @@ public class MediaType private constructor( public val isRwpm: Boolean get() = matchesAny(READIUM_AUDIOBOOK_MANIFEST, DIVINA_MANIFEST, READIUM_WEBPUB_MANIFEST) + public val isRpf: Boolean get() = matchesAny( + READIUM_WEBPUB, + READIUM_AUDIOBOOK, + LCP_PROTECTED_PDF, + LCP_PROTECTED_AUDIOBOOK + ) + /** Returns whether this media type is of a publication file. */ public val isPublication: Boolean get() = matchesAny( READIUM_AUDIOBOOK, READIUM_AUDIOBOOK_MANIFEST, CBZ, DIVINA, DIVINA_MANIFEST, EPUB, LCP_PROTECTED_AUDIOBOOK, @@ -284,6 +291,7 @@ public class MediaType private constructor( public val AVIF: MediaType = MediaType("image/avif")!! public val BINARY: MediaType = MediaType("application/octet-stream")!! public val BMP: MediaType = MediaType("image/bmp")!! + public val CBR: MediaType = MediaType("application/vnd.comicbook-rar")!! public val CBZ: MediaType = MediaType("application/vnd.comicbook+zip")!! public val CSS: MediaType = MediaType("text/css")!! public val DIVINA: MediaType = MediaType("application/divina+zip")!! @@ -330,6 +338,7 @@ public class MediaType private constructor( public val OTF: MediaType = MediaType("font/otf")!! public val PDF: MediaType = MediaType("application/pdf")!! public val PNG: MediaType = MediaType("image/png")!! + public val RAR: MediaType = MediaType("application/vnd.rar")!! public val READIUM_AUDIOBOOK: MediaType = MediaType("application/audiobook+zip")!! public val READIUM_AUDIOBOOK_MANIFEST: MediaType = MediaType("application/audiobook+json")!! public val READIUM_WEBPUB: MediaType = MediaType("application/webpub+zip")!! diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index 9211d621e8..9738803416 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -59,7 +59,7 @@ public interface BlobMediaTypeSniffer { public interface ContainerMediaTypeSniffer { public suspend fun sniffContainer( - container: Container<*> + container: Container ): Try } @@ -91,7 +91,7 @@ public interface MediaTypeSniffer : * Sniffs a [MediaType] from a [Container]. */ public override suspend fun sniffContainer( - container: Container<*> + container: Container ): Try = Try.failure(MediaTypeSnifferError.NotRecognized) } @@ -129,7 +129,7 @@ public class CompositeMediaTypeSniffer( return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContainer(container: Container<*>): Try { + override suspend fun sniffContainer(container: Container): Try { for (sniffer in sniffers) { sniffer.sniffContainer(container) .getOrElse { error -> @@ -531,7 +531,7 @@ public object WebPubMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContainer(container: Container<*>): Try { + override suspend fun sniffContainer(container: Container): Try { // Reads a RWPM from a manifest.json archive entry. val manifest: Manifest = container[RelativeUrl("manifest.json")!!] @@ -611,7 +611,7 @@ public object EpubMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContainer(container: Container<*>): Try { + override suspend fun sniffContainer(container: Container): Try { val mimetype = container[RelativeUrl("mimetype")!!] ?.readAsString(charset = Charsets.US_ASCII) ?.getOrElse { error -> @@ -649,7 +649,7 @@ public object LpfMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContainer(container: Container<*>): Try { + override suspend fun sniffContainer(container: Container): Try { if (RelativeUrl("index.html")!! in container) { return Try.success(MediaType.LPF) } @@ -674,6 +674,22 @@ public object LpfMediaTypeSniffer : MediaTypeSniffer { } } +public object RarMediaTypeSniffer : MediaTypeSniffer { + + override fun sniffHints(hints: MediaTypeHints): Try { + if ( + hints.hasFileExtension("rar") || + hints.hasMediaType("application/vnd.rar") || + hints.hasMediaType("application/x-rar") || + hints.hasMediaType("application/x-rar-compressed") + ) { + return Try.success(MediaType.LPF) + } + + return Try.failure(MediaTypeSnifferError.NotRecognized) + } +} + /** * Sniffs a simple Archive-based format, like Comic Book Archive or Zipped Audio Book. * @@ -729,12 +745,20 @@ public object ArchiveMediaTypeSniffer : MediaTypeSniffer { hints.hasFileExtension("cbz") || hints.hasMediaType( "application/vnd.comicbook+zip", - "application/x-cbz", - "application/x-cbr" + "application/x-cbz" ) ) { return Try.success(MediaType.CBZ) } + + if ( + hints.hasFileExtension("cbr") || + hints.hasMediaType("application/vnd.comicbook-rar") || + hints.hasMediaType("application/x-cbr") + ) { + return Try.success(MediaType.CBR) + } + if (hints.hasFileExtension("zab")) { return Try.success(MediaType.ZAB) } @@ -742,7 +766,7 @@ public object ArchiveMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffContainer(container: Container<*>): Try { + override suspend fun sniffContainer(container: Container): Try { fun isIgnored(url: Url): Boolean = url.filename?.startsWith(".") == true || url.filename == "Thumbs.db" @@ -755,10 +779,24 @@ public object ArchiveMediaTypeSniffer : MediaTypeSniffer { } == true } - if (archiveContainsOnlyExtensions(cbzExtensions)) { + if ( + archiveContainsOnlyExtensions(cbzExtensions) && + container.archiveMediaType?.matches(MediaType.ZIP) == true + ) { return Try.success(MediaType.CBZ) } - if (archiveContainsOnlyExtensions(zabExtensions)) { + + if ( + archiveContainsOnlyExtensions(cbzExtensions) && + container.archiveMediaType?.matches(MediaType.RAR) == true + ) { + return Try.success(MediaType.CBR) + } + + if ( + archiveContainsOnlyExtensions(zabExtensions) && + container.archiveMediaType?.matches(MediaType.ZIP) == true + ) { return Try.success(MediaType.ZAB) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobMediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobMediaTypeRetriever.kt deleted file mode 100644 index 695c4cfd91..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BlobMediaTypeRetriever.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.resource - -import android.content.ContentResolver -import android.provider.MediaStore -import java.io.File -import org.readium.r2.shared.DelicateReadiumApi -import org.readium.r2.shared.extensions.queryProjection -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Readable -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints -import org.readium.r2.shared.util.mediatype.MediaTypeSniffer -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.mediatype.SystemMediaTypeSniffer -import org.readium.r2.shared.util.toUri - -@DelicateReadiumApi -public class BlobMediaTypeRetriever( - private val mediaTypeSniffer: MediaTypeSniffer, - private val contentResolver: ContentResolver? -) { - - /** - * Retrieves a canonical [MediaType] for the provided media type and file extension [hints]. - */ - public fun retrieve(hints: MediaTypeHints): MediaType? { - mediaTypeSniffer.sniffHints(hints) - .getOrNull() - ?.let { return it } - - // Falls back on the system-wide registered media types using MimeTypeMap. - // Note: This is done after the default sniffers, because otherwise it will detect - // JSON, XML or ZIP formats before we have a chance of sniffing their content (for example, - // for RWPM). - SystemMediaTypeSniffer.sniffHints(hints) - .getOrNull() - ?.let { return it } - - return hints.mediaTypes.firstOrNull() - } - - public suspend fun retrieve(hints: MediaTypeHints, readable: Readable): Try { - mediaTypeSniffer.sniffBlob(readable) - .onSuccess { return Try.success(it) } - .onFailure { error -> - when (error) { - is MediaTypeSnifferError.NotRecognized -> {} - else -> return Try.failure(error) - } - } - - // Falls back on the system-wide registered media types using MimeTypeMap. - // Note: This is done after the default sniffers, because otherwise it will detect - // JSON, XML or ZIP formats before we have a chance of sniffing their content (for example, - // for RWPM). - SystemMediaTypeSniffer.sniffHints(hints) - .getOrNull() - ?.let { return Try.success(it) } - - SystemMediaTypeSniffer.sniffBlob(readable) - .onSuccess { return Try.success(it) } - .onFailure { error -> - when (error) { - is MediaTypeSnifferError.NotRecognized -> {} - else -> return Try.failure(error) - } - } - - // Falls back on the [contentResolver] in case of content Uri. - // Note: This is done after the heavy sniffing of the provided [sniffers], because - // otherwise it will detect JSON, XML or ZIP formats before we have a chance of sniffing - // their content (for example, for RWPM). - - if (contentResolver != null) { - (readable as Resource).source - ?.takeIf { it.isContent } - ?.let { url -> - val contentHints = MediaTypeHints( - mediaType = contentResolver.getType(url.toUri()) - ?.let { MediaType(it) } - ?.takeUnless { it.matches(MediaType.BINARY) }, - fileExtension = contentResolver - .queryProjection(url.uri, MediaStore.MediaColumns.DISPLAY_NAME) - ?.let { filename -> File(filename).extension } - ) - - retrieve(contentHints) - ?.let { return Try.success(it) } - } - } - - return hints.mediaTypes.firstOrNull() - ?.let { Try.success(it) } - ?: Try.failure(MediaTypeSnifferError.NotRecognized) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index 9b687858b0..70de63c953 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -35,12 +35,13 @@ public class DirectoryContainer( public companion object { public suspend operator fun invoke(root: File): Try { + val rootUrl = root.toUrl() val entries = try { withContext(Dispatchers.IO) { root.walk() .filter { it.isFile } - .map { it.toUrl() } + .map { rootUrl.relativize(it.toUrl()) } .toSet() } } catch (e: SecurityException) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index d45ea4d0d8..1cb3dac847 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -15,13 +15,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.resource.ArchiveFactory import org.readium.r2.shared.util.resource.Resource /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt index 284d543e47..5a97cf4ce2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt @@ -20,14 +20,15 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.archive.ArchiveProperties +import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream -import org.readium.r2.shared.util.resource.ArchiveProperties +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.toUrl internal class FileZipContainer( @@ -126,6 +127,8 @@ internal class FileZipContainer( } } + override val archiveMediaType: MediaType = MediaType.ZIP + override val source: AbsoluteUrl = file.toUrl() override val entries: Set = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 56f8c1732c..9180e07022 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -14,13 +14,13 @@ import org.readium.r2.shared.extensions.unwrapInstance import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.resource.ArchiveFactory import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toUrl diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt index 8e33220c94..78459a5b0e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt @@ -8,7 +8,6 @@ package org.readium.r2.shared.util.zip import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.readium.r2.shared.DelicateReadiumApi import org.readium.r2.shared.extensions.readFully import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.extensions.unwrapInstance @@ -17,20 +16,20 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.archive.ArchiveProperties +import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream -import org.readium.r2.shared.util.resource.ArchiveProperties +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry -import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.resource.filename import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipArchiveEntry import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile -@OptIn(DelicateReadiumApi::class) internal class StreamingZipContainer( private val zipFile: ZipFile, override val source: AbsoluteUrl? @@ -141,6 +140,8 @@ internal class StreamingZipContainer( } } + override val archiveMediaType: MediaType = MediaType.ZIP + override val entries: Set = zipFile.entries.toList() .filterNot { it.isDirectory } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt index 56cb7a6bac..30c43f7531 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt @@ -7,10 +7,10 @@ package org.readium.r2.shared.util.zip import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ArchiveFactory import org.readium.r2.shared.util.resource.Resource public class ZipArchiveFactory : ArchiveFactory { diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt index fdcc4411db..cf297e4922 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt @@ -16,15 +16,11 @@ import org.junit.runner.RunWith import org.readium.r2.shared.publication.Publication.Profile import org.readium.r2.shared.publication.services.DefaultLocatorService import org.readium.r2.shared.publication.services.PositionsService -import org.readium.r2.shared.publication.services.WebPositionsService import org.readium.r2.shared.publication.services.positions import org.readium.r2.shared.publication.services.positionsByReadingOrder import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.StringBlob +import org.readium.r2.shared.util.data.EmptyContainer import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.EmptyContainer -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.readAsString import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -383,27 +379,6 @@ class PublicationTest { assertNull(createPublication().linkWithHref(Url("foobar")!!)) } - @Test fun `get method passes on href parameters to services`() { - val service = object : Publication.Service { - override fun get(href: Url): Resource { - assertEquals("link?param1=a¶m2=b", href.toString()) - return StringBlob("test passed", MediaType.TEXT) - } - } - - val link = Link(href = Href("link?param1=a¶m2=b")!!) - val publication = createPublication( - resources = listOf(link), - servicesBuilder = Publication.ServicesBuilder( - positions = { service } - ) - ) - assertEquals( - "test passed", - runBlocking { publication.get(link).readAsString().getOrNull() } - ) - } - @Test fun `find the first resource {Link} with the given {href}`() { val link1 = Link(href = Href("href1")!!) val link2 = Link(href = Href("href2")!!) @@ -458,9 +433,8 @@ class ServicesBuilderTest { fun testBuildEmpty() { val builder = Publication.ServicesBuilder(cover = null) val services = builder.build(context) - assertEquals(2, services.size) + assertEquals(1, services.size) assertNotNull(services.find()) - assertNotNull(services.find()) } @Test diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtectionTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtectionTest.kt index 7e4e521879..3ec624a9d6 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtectionTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtectionTest.kt @@ -11,8 +11,11 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith +import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @@ -21,7 +24,7 @@ class AdeptFallbackContentProtectionTest { @Test fun `Sniff no content protection`() { - assertFalse(supports(mediaType = MediaType.EPUB, resources = emptyMap())) + assertFalse(supports(mediaType = MediaType.EPUB, resources = emptyMap()).assertSuccess()) } @Test @@ -32,7 +35,7 @@ class AdeptFallbackContentProtectionTest { resources = mapOf( "META-INF/encryption.xml" to """""" ) - ) + ).assertSuccess() ) } @@ -54,7 +57,7 @@ class AdeptFallbackContentProtectionTest { """ ) - ) + ).assertSuccess() ) } @@ -67,15 +70,14 @@ class AdeptFallbackContentProtectionTest { "META-INF/encryption.xml" to """""", "META-INF/rights.xml" to """""" ) - ) + ).assertSuccess() ) } - private fun supports(mediaType: MediaType, resources: Map): Boolean = runBlocking { + private fun supports(mediaType: MediaType, resources: Map): Try = runBlocking { AdeptFallbackContentProtection().supports( - org.readium.r2.shared.util.asset.Asset.Container( + Asset.Container( mediaType = mediaType, - exploded = false, container = TestContainer(resources.mapKeys { Url(it.key)!! }) ) ) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt index 2ec5321cf0..5fc4853ead 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt @@ -11,9 +11,11 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith +import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -21,7 +23,7 @@ class LcpFallbackContentProtectionTest { @Test fun `Sniff no content protection`() { - assertFalse(supports(mediaType = MediaType.EPUB, resources = emptyMap())) + assertFalse(supports(mediaType = MediaType.EPUB, resources = emptyMap()).assertSuccess()) } @Test @@ -32,7 +34,7 @@ class LcpFallbackContentProtectionTest { resources = mapOf( "META-INF/encryption.xml" to """""" ) - ) + ).assertSuccess() ) } @@ -44,7 +46,7 @@ class LcpFallbackContentProtectionTest { resources = mapOf( "license.lcpl" to "{}" ) - ) + ).assertSuccess() ) } @@ -56,7 +58,7 @@ class LcpFallbackContentProtectionTest { resources = mapOf( "META-INF/license.lcpl" to "{}" ) - ) + ).assertSuccess() ) } @@ -84,15 +86,14 @@ class LcpFallbackContentProtectionTest { """ ) - ) + ).assertSuccess() ) } - private fun supports(mediaType: MediaType, resources: Map): Boolean = runBlocking { - LcpFallbackContentProtection(MediaTypeRetriever()).supports( + private fun supports(mediaType: MediaType, resources: Map): Try = runBlocking { + LcpFallbackContentProtection().supports( org.readium.r2.shared.util.asset.Asset.Container( mediaType = mediaType, - exploded = false, container = TestContainer(resources.mapKeys { Url(it.key)!! }) ) ) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt index f7246808e2..085f620231 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt @@ -6,52 +6,20 @@ package org.readium.r2.shared.publication.protection -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.data.StringBlob -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Container +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceTry +import org.readium.r2.shared.util.resource.StringResource -class TestContainer(resources: Map = emptyMap()) : Container { +class TestContainer( + private val resources: Map = emptyMap() +) : Container { - private val entries: Map = - resources.mapValues { Entry(it.key, StringBlob(it.value, MediaType.TEXT)) } + override val entries: Set = + resources.keys - override suspend fun entries(): Set = - entries.values.toSet() - - override fun get(url: Url): Container.Entry = - entries[url] ?: NotFoundEntry(url) + override fun get(url: Url): Resource? = + resources[url]?.let { StringResource(it) } override suspend fun close() {} - - private class NotFoundEntry( - override val url: Url - ) : Container.Entry { - - override val source: AbsoluteUrl? = null - - override suspend fun mediaType(): ResourceTry = - Try.failure(Resource.Error.NotFound()) - - override suspend fun properties(): ResourceTry = - Try.failure(Resource.Error.NotFound()) - - override suspend fun length(): ResourceTry = - Try.failure(Resource.Error.NotFound()) - - override suspend fun read(range: LongRange?): ResourceTry = - Try.failure(Resource.Error.NotFound()) - - override suspend fun close() { - } - } - - private class Entry( - override val url: Url, - private val resource: StringBlob - ) : Resource by resource, Container.Entry } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt index 3f7fafd821..d3f7214c10 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt @@ -19,12 +19,10 @@ import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.publication.* -import org.readium.r2.shared.readBlocking import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.FileResource -import org.readium.r2.shared.util.resource.ResourceContainer +import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.shared.util.toAbsoluteUrl import org.robolectric.RobolectricTestRunner @@ -59,25 +57,13 @@ class CoverServiceTest { Link(href = Href(coverPath), rels = setOf("cover")) ) ), - container = ResourceContainer( + container = SingleResourceContainer( coverPath, FileResource(coverPath.toFile()!!, mediaType = MediaType.JPEG) ) ) } - @Test - fun `get works fine`() = runBlocking { - val service = InMemoryCoverService(coverBitmap) - val res = service.get(Url("/~readium/cover")!!) - assertNotNull(res) - - val bytes = res.readBlocking().getOrNull() - assertNotNull(bytes) - - assertTrue(BitmapFactory.decodeByteArray(bytes, 0, bytes.size).sameAs(coverBitmap)) - } - @Test fun `helper for ServicesBuilder works fine`() { val factory = { _: Publication.Service.Context -> diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/PositionsServiceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/PositionsServiceTest.kt index edfd0fbbd2..419fa090e4 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/PositionsServiceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/PositionsServiceTest.kt @@ -11,89 +11,20 @@ package org.readium.r2.shared.publication.services import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking -import org.json.JSONObject import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith -import org.readium.r2.shared.extensions.mapNotNull -import org.readium.r2.shared.extensions.optNullableInt import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.readAsString import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class PositionsServiceTest { - @Test - fun `get works fine`() { - val positions = listOf( - listOf( - Locator( - href = Url("res")!!, - mediaType = MediaType.XML, - locations = Locator.Locations( - position = 1, - totalProgression = 0.0 - ) - ) - ), - listOf( - Locator( - href = Url("chap1")!!, - mediaType = MediaType.PNG, - locations = Locator.Locations( - position = 2, - totalProgression = 1.0 / 4.0 - ) - ) - ), - listOf( - Locator( - href = Url("chap2")!!, - mediaType = MediaType.PNG, - title = "Chapter 2", - locations = Locator.Locations( - position = 3, - totalProgression = 3.0 / 4.0 - ) - ), - Locator( - href = Url("chap2")!!, - mediaType = MediaType.PNG, - title = "Chapter 2.5", - locations = Locator.Locations( - position = 4, - totalProgression = 3.0 / 4.0 - ) - ) - ) - ) - - val service = object : PositionsService { - override suspend fun positionsByReadingOrder(): List> = positions - } - - val json = service.handle(Url("/~readium/positions")!!) - ?.let { runBlocking { it.readAsString() } } - ?.getOrNull() - ?.let { JSONObject(it) } - val total = json - ?.optNullableInt("total") - val locators = json - ?.optJSONArray("positions") - ?.mapNotNull { locator -> - (locator as? JSONObject)?.let { Locator.fromJSON(it) } - } - - assertEquals(positions.flatten().size, total) - assertEquals(positions.flatten(), locators) - } - @Test fun `helper for ServicesBuilder works fine`() { val factory = { _: Publication.Service.Context -> diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIteratorTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIteratorTest.kt index caca0f3012..1969f5ab7a 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIteratorTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIteratorTest.kt @@ -19,15 +19,14 @@ import org.readium.r2.shared.publication.services.content.Content.TextElement import org.readium.r2.shared.publication.services.content.Content.TextElement.Segment import org.readium.r2.shared.util.Language import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.StringBlob import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.StringResource import org.robolectric.RobolectricTestRunner @OptIn(ExperimentalReadiumApi::class) @RunWith(RobolectricTestRunner::class) class HtmlResourceContentIteratorTest { - private val link = Link(href = Href("/dir/res.xhtml")!!, mediaType = MediaType.XHTML) private val locator = Locator(href = Url("/dir/res.xhtml")!!, mediaType = MediaType.XHTML) private val html = """ @@ -182,7 +181,7 @@ class HtmlResourceContentIteratorTest { totalProgressionRange: ClosedRange? = null ): HtmlResourceContentIterator = HtmlResourceContentIterator( - StringBlob(html, MediaType.HTML), + StringResource(html), totalProgressionRange = totalProgressionRange, startLocator ) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt index 850051ed1e..619c464853 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt @@ -20,8 +20,17 @@ class FormatRegistryTest { fun `register new file extensions`() = runBlocking { val mediaType = MediaType("application/test")!! val sut = sut() - sut.register(mediaType, fileExtension = "tst") + sut.register(mediaType, fileExtension = "tst", superType = null) assertEquals(sut.fileExtension(mediaType), "tst") } + + @Test + fun `register new format with supertype`() = runBlocking { + val mediaType = MediaType("application/test")!! + val sut = sut() + sut.register(mediaType, fileExtension = null, superType = MediaType.ZIP) + + assertEquals(sut.superType(mediaType), MediaType.ZIP) + } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt index fe46327cd4..daa29c865b 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt @@ -1,17 +1,17 @@ package org.readium.r2.shared.util.mediatype import android.webkit.MimeTypeMap -import java.io.File import kotlin.test.assertEquals -import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures -import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.MediaTypeRetriever -import org.readium.r2.shared.util.zip.FileZipArchiveProvider +import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.asset.DefaultMediaTypeSniffer +import org.readium.r2.shared.util.asset.MediaTypeRetriever +import org.readium.r2.shared.util.resource.StringResource +import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf @@ -20,7 +20,12 @@ class MediaTypeRetrieverTest { val fixtures = Fixtures("util/mediatype") - private val retriever = MediaTypeRetriever() + private val retriever = MediaTypeRetriever( + DefaultMediaTypeSniffer(), + FormatRegistry(), + ZipArchiveFactory(), + null + ) @Test fun `sniff ignores extension case`() = runBlocking { @@ -79,14 +84,17 @@ class MediaTypeRetrieverTest { fun `sniff from bytes`() = runBlocking { assertEquals( MediaType.READIUM_AUDIOBOOK_MANIFEST, - retriever.retrieveResource(fixtures.fileAt("audiobook.json")) + retriever.retrieve(fixtures.fileAt("audiobook.json")).assertSuccess() ) } @Test fun `sniff unknown format`() = runBlocking { assertNull(retriever.retrieve(mediaType = "invalid")) - assertNull(retriever.retrieveResource(fixtures.fileAt("unknown"))) + assertEquals( + retriever.retrieve(fixtures.fileAt("unknown")).failureOrNull(), + MediaTypeSnifferError.NotRecognized + ) } @Test @@ -101,7 +109,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.READIUM_AUDIOBOOK, - retriever.retrieveArchive(fixtures.fileAt("audiobook-package.unknown")) + retriever.retrieve(fixtures.fileAt("audiobook-package.unknown")).assertSuccess() ) } @@ -113,11 +121,11 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.READIUM_AUDIOBOOK_MANIFEST, - retriever.retrieveResource(fixtures.fileAt("audiobook.json")) + retriever.retrieve(fixtures.fileAt("audiobook.json")).assertSuccess() ) assertEquals( MediaType.READIUM_AUDIOBOOK_MANIFEST, - retriever.retrieveResource(fixtures.fileAt("audiobook-wrongtype.json")) + retriever.retrieve(fixtures.fileAt("audiobook-wrongtype.json")).assertSuccess() ) } @@ -137,8 +145,12 @@ class MediaTypeRetrieverTest { retriever.retrieve(mediaType = "application/vnd.comicbook+zip") ) assertEquals(MediaType.CBZ, retriever.retrieve(mediaType = "application/x-cbz")) - assertEquals(MediaType.CBZ, retriever.retrieve(mediaType = "application/x-cbr")) - assertEquals(MediaType.CBZ, retriever.retrieveArchive(fixtures.fileAt("cbz.unknown"))) + assertEquals(MediaType.CBR, retriever.retrieve(mediaType = "application/x-cbr")) + + assertEquals( + MediaType.CBZ, + retriever.retrieve(fixtures.fileAt("cbz.unknown")).assertSuccess() + ) } @Test @@ -150,7 +162,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.DIVINA, - retriever.retrieveArchive(fixtures.fileAt("divina-package.unknown")) + retriever.retrieve(fixtures.fileAt("divina-package.unknown")).assertSuccess() ) } @@ -162,7 +174,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.DIVINA_MANIFEST, - retriever.retrieveResource(fixtures.fileAt("divina.json")) + retriever.retrieve(fixtures.fileAt("divina.json")).assertSuccess() ) } @@ -173,7 +185,10 @@ class MediaTypeRetrieverTest { MediaType.EPUB, retriever.retrieve(mediaType = "application/epub+zip") ) - assertEquals(MediaType.EPUB, retriever.retrieveArchive(fixtures.fileAt("epub.unknown"))) + assertEquals( + MediaType.EPUB, + retriever.retrieve(fixtures.fileAt("epub.unknown")).assertSuccess() + ) } @Test @@ -193,10 +208,13 @@ class MediaTypeRetrieverTest { assertEquals(MediaType.HTML, retriever.retrieve(fileExtension = "htm")) assertEquals(MediaType.HTML, retriever.retrieve(fileExtension = "html")) assertEquals(MediaType.HTML, retriever.retrieve(mediaType = "text/html")) - assertEquals(MediaType.HTML, retriever.retrieveResource(fixtures.fileAt("html.unknown"))) assertEquals( MediaType.HTML, - retriever.retrieveResource(fixtures.fileAt("html-doctype-case.unknown")) + retriever.retrieve(fixtures.fileAt("html.unknown")).assertSuccess() + ) + assertEquals( + MediaType.HTML, + retriever.retrieve(fixtures.fileAt("html-doctype-case.unknown")).assertSuccess() ) } @@ -208,7 +226,10 @@ class MediaTypeRetrieverTest { MediaType.XHTML, retriever.retrieve(mediaType = "application/xhtml+xml") ) - assertEquals(MediaType.XHTML, retriever.retrieveResource(fixtures.fileAt("xhtml.unknown"))) + assertEquals( + MediaType.XHTML, + retriever.retrieve(fixtures.fileAt("xhtml.unknown")).assertSuccess() + ) } @Test @@ -244,7 +265,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.OPDS1, - retriever.retrieveResource(fixtures.fileAt("opds1-feed.unknown")) + retriever.retrieve(fixtures.fileAt("opds1-feed.unknown")).assertSuccess() ) } @@ -258,7 +279,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.OPDS1_ENTRY, - retriever.retrieveResource(fixtures.fileAt("opds1-entry.unknown")) + retriever.retrieve(fixtures.fileAt("opds1-entry.unknown")).assertSuccess() ) } @@ -270,7 +291,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.OPDS2, - retriever.retrieveResource(fixtures.fileAt("opds2-feed.json")) + retriever.retrieve(fixtures.fileAt("opds2-feed.json")).assertSuccess() ) } @@ -282,7 +303,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.OPDS2_PUBLICATION, - retriever.retrieveResource(fixtures.fileAt("opds2-publication.json")) + retriever.retrieve(fixtures.fileAt("opds2-publication.json")).assertSuccess() ) } @@ -298,7 +319,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.OPDS_AUTHENTICATION, - retriever.retrieveResource(fixtures.fileAt("opds-authentication.json")) + retriever.retrieve(fixtures.fileAt("opds-authentication.json")).assertSuccess() ) } @@ -314,7 +335,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.LCP_PROTECTED_AUDIOBOOK, - retriever.retrieveArchive(fixtures.fileAt("audiobook-lcp.unknown")) + retriever.retrieve(fixtures.fileAt("audiobook-lcp.unknown")).assertSuccess() ) } @@ -330,7 +351,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.LCP_PROTECTED_PDF, - retriever.retrieveArchive(fixtures.fileAt("pdf-lcp.unknown")) + retriever.retrieve(fixtures.fileAt("pdf-lcp.unknown")).assertSuccess() ) } @@ -346,7 +367,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.LCP_LICENSE_DOCUMENT, - retriever.retrieveResource(fixtures.fileAt("lcpl.unknown")) + retriever.retrieve(fixtures.fileAt("lcpl.unknown")).assertSuccess() ) } @@ -354,10 +375,13 @@ class MediaTypeRetrieverTest { fun `sniff LPF`() = runBlocking { assertEquals(MediaType.LPF, retriever.retrieve(fileExtension = "lpf")) assertEquals(MediaType.LPF, retriever.retrieve(mediaType = "application/lpf+zip")) - assertEquals(MediaType.LPF, retriever.retrieveArchive(fixtures.fileAt("lpf.unknown"))) assertEquals( MediaType.LPF, - retriever.retrieveArchive(fixtures.fileAt("lpf-index-html.unknown")) + retriever.retrieve(fixtures.fileAt("lpf.unknown")).assertSuccess() + ) + assertEquals( + MediaType.LPF, + retriever.retrieve(fixtures.fileAt("lpf-index-html.unknown")).assertSuccess() ) } @@ -365,7 +389,10 @@ class MediaTypeRetrieverTest { fun `sniff PDF`() = runBlocking { assertEquals(MediaType.PDF, retriever.retrieve(fileExtension = "pdf")) assertEquals(MediaType.PDF, retriever.retrieve(mediaType = "application/pdf")) - assertEquals(MediaType.PDF, retriever.retrieveResource(fixtures.fileAt("pdf.unknown"))) + assertEquals( + MediaType.PDF, + retriever.retrieve(fixtures.fileAt("pdf.unknown")).assertSuccess() + ) } @Test @@ -400,7 +427,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.READIUM_WEBPUB, - retriever.retrieveArchive(fixtures.fileAt("webpub-package.unknown")) + retriever.retrieve(fixtures.fileAt("webpub-package.unknown")).assertSuccess() ) } @@ -412,7 +439,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.READIUM_WEBPUB_MANIFEST, - retriever.retrieveResource(fixtures.fileAt("webpub.json")) + retriever.retrieve(fixtures.fileAt("webpub.json")).assertSuccess() ) } @@ -420,19 +447,25 @@ class MediaTypeRetrieverTest { fun `sniff W3C WPUB manifest`() = runBlocking { assertEquals( MediaType.W3C_WPUB_MANIFEST, - retriever.retrieveResource(fixtures.fileAt("w3c-wpub.json")) + retriever.retrieve(fixtures.fileAt("w3c-wpub.json")).assertSuccess() ) } @Test fun `sniff ZAB`() = runBlocking { assertEquals(MediaType.ZAB, retriever.retrieve(fileExtension = "zab")) - assertEquals(MediaType.ZAB, retriever.retrieveArchive(fixtures.fileAt("zab.unknown"))) + assertEquals( + MediaType.ZAB, + retriever.retrieve(fixtures.fileAt("zab.unknown")).assertSuccess() + ) } @Test fun `sniff JSON`() = runBlocking { - assertEquals(MediaType.JSON, retriever.retrieveResource(fixtures.fileAt("any.json"))) + assertEquals( + MediaType.JSON, + retriever.retrieve(fixtures.fileAt("any.json")).assertSuccess() + ) } @Test @@ -450,9 +483,9 @@ class MediaTypeRetrieverTest { assertEquals( MediaType.JSON_PROBLEM_DETAILS, retriever.retrieve( - hints = MediaTypeHints(mediaType = MediaType("application/problem+json")!!), - content = BytesResourceMediaTypeSnifferContent { """{"title": "Message"}""".toByteArray() } - ) + resource = StringResource("""{"title": "Message"}"""), + hints = MediaTypeHints(mediaType = MediaType("application/problem+json")!!) + ).assertSuccess() ) } @@ -486,29 +519,6 @@ class MediaTypeRetrieverTest { fun `sniff system media types from bytes`() = runBlocking { shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("png", "image/png") val png = MediaType("image/png")!! - assertEquals(png, retriever.retrieveResource(fixtures.fileAt("png.unknown"))) - } - - // Convenience - - private suspend fun MediaTypeRetriever.retrieveResource(file: File): MediaType? = - retrieve(content = BytesResourceMediaTypeSnifferContent { file.readBytes() }) - - private suspend fun MediaTypeRetriever.retrieveArchive( - file: File, - hints: MediaTypeHints = MediaTypeHints() - ): MediaType? { - val archive = assertNotNull(FileZipArchiveProvider(this).open(file).getOrNull()) - - return retrieve( - hints, - content = object : ContainerMediaTypeSnifferContent { - override suspend fun entries(): Set? = - archive.entries()?.map { it.url.toString() }?.toSet() - - override suspend fun read(path: String, range: LongRange?): ByteArray? = - archive.get(Url(path)!!).read(range).getOrNull() - } - ) + assertEquals(png, retriever.retrieve(fixtures.fileAt("png.unknown")).assertSuccess()) } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt index 1bf64d0fdc..9fa63a2b3c 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures +import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @@ -19,8 +20,8 @@ class BufferingResourceTest { } @Test - fun `get media type`() = runBlocking { - assertEquals(MediaType.EPUB, sut().mediaType().getOrNull()) + fun `get properties`() = runBlocking { + assertEquals(resource.properties().assertSuccess(), sut().properties().assertSuccess()) } @Test diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt index 8f54c3d55a..0e184d2266 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt @@ -12,8 +12,8 @@ package org.readium.r2.shared.util.resource import android.webkit.MimeTypeMap import java.nio.charset.StandardCharsets import kotlin.test.assertEquals -import kotlin.test.assertIs import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -21,7 +21,8 @@ import org.junit.runner.RunWith import org.readium.r2.shared.lengthBlocking import org.readium.r2.shared.readBlocking import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.toAbsoluteUrl import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows @@ -30,57 +31,54 @@ import org.robolectric.Shadows class DirectoryContainerTest { private val directory = assertNotNull( - DirectoryContainerTest::class.java.getResource("directory")?.toAbsoluteUrl() + DirectoryContainerTest::class.java.getResource("directory")?.toAbsoluteUrl()?.toFile() ) - private fun sut(): Container = runBlocking { + private fun sut(): Container = runBlocking { assertNotNull( - DirectoryContainerFactory(MediaTypeRetriever()).create(directory).getOrNull() + DirectoryContainer(directory).assertSuccess() ) } @Test - fun `Reading a missing file returns NotFound`() { - val resource = sut().get(Url("unknown")!!) - assertIs(resource.readBlocking().failureOrNull()) + fun `Reading a missing file returns null`() { + assertNull(sut()[Url("unknown")!!]) } @Test fun `Reading a file at the root works well`() { - val resource = sut().get(Url("text1.txt")!!) + val resource = assertNotNull(sut()[Url("text1.txt")!!]) val result = resource.readBlocking().getOrNull() assertEquals("text1", result?.toString(StandardCharsets.UTF_8)) } @Test fun `Reading a file in a subdirectory works well`() { - val resource = sut().get(Url("subdirectory/text2.txt")!!) + val resource = assertNotNull(sut()[Url("subdirectory/text2.txt")!!]) val result = resource.readBlocking().getOrNull() assertEquals("text2", result?.toString(StandardCharsets.UTF_8)) } @Test - fun `Reading a directory returns NotFound`() { - val resource = sut().get(Url("subdirectory")!!) - assertIs(resource.readBlocking().failureOrNull()) + fun `Reading a directory returns null`() { + assertNull(sut()[Url("subdirectory")!!]) } @Test - fun `Reading a file outside the allowed directory returns NotFound`() { - val resource = sut().get(Url("../epub.epub")!!) - assertIs(resource.readBlocking().failureOrNull()) + fun `Reading a file outside the allowed directory returns null`() { + assertNull(sut()[Url("../epub.epub")!!]) } @Test fun `Reading a range works well`() { - val resource = sut().get(Url("text1.txt")!!) - val result = resource.readBlocking(0..2L).getOrNull() - assertEquals("tex", result?.toString(StandardCharsets.UTF_8)) + val resource = assertNotNull(sut()[Url("text1.txt")!!]) + val result = assertNotNull(resource.readBlocking(0..2L).getOrNull()) + assertEquals("tex", result.toString(StandardCharsets.UTF_8)) } @Test fun `Reading two ranges with the same resource work well`() { - val resource = sut().get(Url("text1.txt")!!) + val resource = assertNotNull(sut()[Url("text1.txt")!!]) val result1 = resource.readBlocking(0..1L).getOrNull() assertEquals("te", result1?.toString(StandardCharsets.UTF_8)) val result2 = resource.readBlocking(1..3L).getOrNull() @@ -89,7 +87,7 @@ class DirectoryContainerTest { @Test fun `Out of range indexes are clamped to the available length`() { - val resource = sut().get(Url("text1.txt")!!) + val resource = assertNotNull(sut()[Url("text1.txt")!!]) val result = resource.readBlocking(-5..60L).getOrNull() assertEquals("text1", result?.toString(StandardCharsets.UTF_8)) assertEquals(5, result?.size) @@ -98,7 +96,7 @@ class DirectoryContainerTest { @Test @Suppress("EmptyRange") fun `Decreasing ranges are understood as empty ones`() { - val resource = sut().get(Url("text1.txt")!!) + val resource = assertNotNull(sut()[Url("text1.txt")!!]) val result = resource.readBlocking(60..20L).getOrNull() assertEquals("", result?.toString(StandardCharsets.UTF_8)) assertEquals(0, result?.size) @@ -106,23 +104,11 @@ class DirectoryContainerTest { @Test fun `Computing length works well`() { - val resource = sut().get(Url("text1.txt")!!) + val resource = assertNotNull(sut().get(Url("text1.txt")!!)) val result = resource.lengthBlocking().getOrNull() assertEquals(5L, result) } - @Test - fun `Computing a directory length returns NotFound`() { - val resource = sut().get(Url("subdirectory")!!) - assertIs(resource.lengthBlocking().failureOrNull()) - } - - @Test - fun `Computing the length of a missing file returns NotFound`() { - val resource = sut().get(Url("unknown")!!) - assertIs(resource.lengthBlocking().failureOrNull()) - } - @Test fun `Computing entries works well`() { runBlocking { @@ -132,11 +118,11 @@ class DirectoryContainerTest { addExtensionMimeTypMapping("mp3", "audio/mpeg") } - val entries = sut().entries() - assertThat(entries?.map { it.url.toString() }).contains( - "subdirectory/hello.mp3", - "subdirectory/text2.txt", - "text1.txt" + val entries = sut().entries + assertThat(entries).contains( + Url("subdirectory/hello.mp3")!!, + Url("subdirectory/text2.txt")!!, + Url("text1.txt")!! ) } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt index 196eb63aee..befffd96b7 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt @@ -6,6 +6,8 @@ import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.assertJSONEquals +import org.readium.r2.shared.util.archive.ArchiveProperties +import org.readium.r2.shared.util.archive.archive import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -13,14 +15,14 @@ class PropertiesTest { @Test fun `get no archive`() { - assertNull(Properties().archive) + assertNull(Resource.Properties().archive) } @Test fun `get full archive`() { assertEquals( ArchiveProperties(entryLength = 8273, isEntryCompressed = true), - Properties( + Resource.Properties( mapOf( "archive" to mapOf( "entryLength" to 8273, @@ -34,7 +36,7 @@ class PropertiesTest { @Test fun `get invalid archive`() { assertNull( - Properties( + Resource.Properties( mapOf( "archive" to mapOf( "foo" to "bar" @@ -47,7 +49,7 @@ class PropertiesTest { @Test fun `get incomplete archive`() { assertNull( - Properties( + Resource.Properties( mapOf( "archive" to mapOf( "isEntryCompressed" to true @@ -57,7 +59,7 @@ class PropertiesTest { ) assertNull( - Properties( + Resource.Properties( mapOf( "archive" to mapOf( "entryLength" to 8273 diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt index 2e5351bd28..b22c505fdc 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt @@ -6,6 +6,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue import org.junit.Test import org.junit.runner.RunWith +import org.readium.r2.shared.util.data.ReadableInputStream import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @@ -21,7 +22,7 @@ class ResourceInputStreamTest { @Test fun `stream can be read by chunks`() { val resource = FileResource(file, mediaType = MediaType.EPUB) - val resourceStream = ResourceInputStream(resource) + val resourceStream = ReadableInputStream(resource) val outputStream = ByteArrayOutputStream(fileContent.size) resourceStream.copyTo(outputStream, bufferSize = bufferSize) assertTrue(fileContent.contentEquals(outputStream.toByteArray())) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt index fa1c717f4f..b061642a10 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -12,44 +12,45 @@ package org.readium.r2.shared.util.resource import java.io.File import java.nio.charset.StandardCharsets import kotlin.test.assertEquals -import kotlin.test.assertFails import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.use -import org.readium.r2.shared.util.zip.FileZipArchiveProvider import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider +import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.robolectric.ParameterizedRobolectricTestRunner @RunWith(ParameterizedRobolectricTestRunner::class) -class ZipContainerTest(val sut: suspend () -> Container) { +class ZipContainerTest(val sut: suspend () -> Container) { companion object { @ParameterizedRobolectricTestRunner.Parameters @JvmStatic - fun archives(): List Container> { + fun archives(): List Container> { val epubZip = ZipContainerTest::class.java.getResource("epub.epub") assertNotNull(epubZip) val zipArchive = suspend { assertNotNull( - FileZipArchiveProvider(MediaTypeRetriever()) + ZipArchiveFactory() .create( - FileResource(File(epubZip.path), mediaType = MediaType.EPUB), - password = null + mediaType = MediaType.ZIP, + FileResource(File(epubZip.path)) ) .getOrNull() ) } val apacheZipArchive = suspend { - StreamingZipArchiveProvider(MediaTypeRetriever()) + StreamingZipArchiveProvider() .openFile(File(epubZip.path)) } @@ -57,9 +58,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { assertNotNull(epubExploded) val explodedArchive = suspend { assertNotNull( - DirectoryContainerFactory(MediaTypeRetriever()) - .create(File(epubExploded.path)) - .getOrNull() + DirectoryContainer(File(epubExploded.path)).assertSuccess() ) } assertNotNull(explodedArchive) @@ -71,18 +70,18 @@ class ZipContainerTest(val sut: suspend () -> Container) { @Test fun `Entry list is correct`(): Unit = runBlocking { sut().use { container -> - assertThat(container.entries()?.map { it.url.toString() }) + assertThat(container.entries) .contains( - "mimetype", - "EPUB/cover.xhtml", - "EPUB/css/epub.css", - "EPUB/css/nav.css", - "EPUB/images/cover.png", - "EPUB/nav.xhtml", - "EPUB/package.opf", - "EPUB/s04.xhtml", - "EPUB/toc.ncx", - "META-INF/container.xml" + Url("mimetype")!!, + Url("EPUB/cover.xhtml")!!, + Url("EPUB/css/epub.css")!!, + Url("EPUB/css/nav.css")!!, + Url("EPUB/images/cover.png")!!, + Url("EPUB/nav.xhtml")!!, + Url("EPUB/package.opf")!!, + Url("EPUB/s04.xhtml")!!, + Url("EPUB/toc.ncx")!!, + Url("META-INF/container.xml")!! ) } } @@ -90,14 +89,15 @@ class ZipContainerTest(val sut: suspend () -> Container) { @Test fun `Attempting to read a missing entry throws`(): Unit = runBlocking { sut().use { container -> - assertFails { container.get(Url("unknown")!!).read().assertSuccess() } + assertNull(container[Url("unknown")!!]) } } @Test fun `Fully reading an entry works well`(): Unit = runBlocking { sut().use { container -> - val bytes = container.get(Url("mimetype")!!).read().assertSuccess() + val resource = assertNotNull(container[Url("mimetype")!!]) + val bytes = resource.read().assertSuccess() assertEquals("application/epub+zip", bytes.toString(StandardCharsets.UTF_8)) } } @@ -105,7 +105,8 @@ class ZipContainerTest(val sut: suspend () -> Container) { @Test fun `Reading a range of an entry works well`(): Unit = runBlocking { sut().use { container -> - val bytes = container.get(Url("mimetype")!!).read(0..10L).assertSuccess() + val resource = assertNotNull(container[Url("mimetype")!!]) + val bytes = resource.read(0..10L).assertSuccess() assertEquals("application", bytes.toString(StandardCharsets.UTF_8)) assertEquals(11, bytes.size) } @@ -114,7 +115,8 @@ class ZipContainerTest(val sut: suspend () -> Container) { @Test fun `Out of range indexes are clamped to the available length`(): Unit = runBlocking { sut().use { container -> - val bytes = container.get(Url("mimetype")!!).read(-5..60L).assertSuccess() + val resource = assertNotNull(container[Url("mimetype")!!]) + val bytes = resource.read(-5..60L).assertSuccess() assertEquals("application/epub+zip", bytes.toString(StandardCharsets.UTF_8)) assertEquals(20, bytes.size) } @@ -123,7 +125,8 @@ class ZipContainerTest(val sut: suspend () -> Container) { @Test fun `Decreasing ranges are understood as empty ones`(): Unit = runBlocking { sut().use { container -> - val bytes = container.get(Url("mimetype")!!).read(60..20L).assertSuccess() + val resource = assertNotNull(container[Url("mimetype")!!]) + val bytes = resource.read(60..20L).assertSuccess() assertEquals("", bytes.toString(StandardCharsets.UTF_8)) assertEquals(0, bytes.size) } @@ -132,7 +135,8 @@ class ZipContainerTest(val sut: suspend () -> Container) { @Test fun `Computing size works well`(): Unit = runBlocking { sut().use { container -> - val size = container.get(Url("mimetype")!!).length().assertSuccess() + val resource = assertNotNull(container[Url("mimetype")!!]) + val size = resource.length().assertSuccess() assertEquals(20L, size) } } diff --git a/readium/streamer/build.gradle.kts b/readium/streamer/build.gradle.kts index 10f34d68fb..bec32e78df 100644 --- a/readium/streamer/build.gradle.kts +++ b/readium/streamer/build.gradle.kts @@ -70,6 +70,7 @@ dependencies { // Tests testImplementation(libs.junit) + testImplementation(libs.kotlin.junit) androidTestImplementation(libs.androidx.ext.junit) androidTestImplementation(libs.androidx.expresso.core) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index dbb73cd546..2ec7324c89 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -16,14 +16,14 @@ import org.readium.r2.shared.publication.protection.LcpFallbackContentProtection import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.DefaultMediaTypeSniffer +import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.logging.WarningLogger -import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.pdf.PdfDocumentFactory -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.readium.r2.streamer.parser.PublicationParser import org.readium.r2.streamer.parser.audio.AudioParser @@ -108,7 +108,7 @@ public class PublicationFactory( contentProtections = contentProtections, mediaTypeRetriever = mediaTypeRetriever, formatRegistry = formatRegistry, - httpClient = DefaultHttpClient(mediaTypeRetriever), + httpClient = DefaultHttpClient(), pdfFactory = null, onCreatePublication = onCreatePublication ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt index 474213b662..36998083d8 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt @@ -24,9 +24,9 @@ internal suspend fun Container<*>.readAsXmlOrNull(path: String): ElementNode? = internal suspend fun Container<*>.readAsXmlOrNull(url: Url): ElementNode? = get(url)?.use { it.readAsXml().getOrNull() } -internal fun Container<*>.guessTitle(): String? { - val firstEntry = entries.firstOrNull() ?: return null - val commonFirstComponent = entries.pathCommonFirstComponent() ?: return null +internal fun Iterable.guessTitle(): String? { + val firstEntry = firstOrNull() ?: return null + val commonFirstComponent = pathCommonFirstComponent() ?: return null if (commonFirstComponent.name == firstEntry.path) { return null diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index 6cdd399188..954eb2329e 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -14,12 +14,12 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs @@ -82,7 +82,7 @@ public class AudioParser( val manifest = Manifest( metadata = Metadata( conformsTo = setOf(Publication.Profile.AUDIOBOOK), - localizedTitle = asset.container.guessTitle()?.let { LocalizedString(it) } + localizedTitle = asset.container.entries.guessTitle()?.let { LocalizedString(it) } ), readingOrder = readingOrderLinks ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt index 2135cfb045..3fb4c54b8a 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt @@ -17,10 +17,10 @@ import org.readium.r2.shared.publication.presentation.Presentation import org.readium.r2.shared.publication.presentation.presentation import org.readium.r2.shared.publication.services.PositionsService import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.use /** diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index 35d5d40d4b..0b2fd4af42 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -15,12 +15,12 @@ import org.readium.r2.shared.publication.services.PerResourcePositionsService import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index 1665d149a2..6609aa4e9b 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -42,10 +42,18 @@ public class ReadiumWebPubParser( return Try.failure(PublicationParser.Error.UnsupportedFormat()) } - val manifest = asset.container - .get(Url("manifest.json")!!) - ?.readAsRwpm() - ?.getOrElse { + val manifestResource = asset.container[Url("manifest.json")!!] + ?: return Try.failure( + PublicationParser.Error.ReadError( + ReadError.Decoding( + MessageError("Missing manifest.") + ) + ) + ) + + val manifest = manifestResource + .readAsRwpm() + .getOrElse { when (it) { is DecoderError.Read -> return Try.failure( @@ -53,6 +61,7 @@ public class ReadiumWebPubParser( ReadError.Decoding(it.cause) ) ) + is DecoderError.Decoding -> return Try.failure( PublicationParser.Error.ReadError( @@ -62,13 +71,7 @@ public class ReadiumWebPubParser( ) ) } - } ?: return Try.failure( - PublicationParser.Error.ReadError( - ReadError.Decoding( - MessageError("Missing manifest.") - ) - ) - ) + } // Checks the requirements from the LCPDF specification. // https://readium.org/lcp-specs/notes/lcp-for-pdf.html diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/ContainerEntryTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/ContainerTest.kt similarity index 51% rename from readium/streamer/src/test/java/org/readium/r2/streamer/extensions/ContainerEntryTest.kt rename to readium/streamer/src/test/java/org/readium/r2/streamer/extensions/ContainerTest.kt index d29069a6cd..5f31104f27 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/ContainerEntryTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/extensions/ContainerTest.kt @@ -13,36 +13,16 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith -import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Container -import org.readium.r2.shared.util.resource.ResourceTry import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class ContainerEntryTest { - - class Entry(path: String) : Container.Entry { - override val url: Url = Url(path)!! - override val source: AbsoluteUrl? = null - override suspend fun mediaType(): ResourceTry = - throw NotImplementedError() - override suspend fun properties(): ResourceTry = - throw NotImplementedError() - override suspend fun length(): ResourceTry = - throw NotImplementedError() - override suspend fun read(range: LongRange?): ResourceTry = - throw NotImplementedError() - override suspend fun close() { - throw NotImplementedError() - } - } +class ContainerTest { @Test fun `pathCommonFirstComponent is null when files are in the root`() { assertNull( - listOf(Entry("im1.jpg"), Entry("im2.jpg"), Entry("toc.xml")) + listOf(Url("im1.jpg")!!, Url("im2.jpg")!!, Url("toc.xml")!!) .pathCommonFirstComponent() ) } @@ -50,7 +30,7 @@ class ContainerEntryTest { @Test fun `pathCommonFirstComponent is null when files are in different directories`() { assertNull( - listOf(Entry("dir1/im1.jpg"), Entry("dir2/im2.jpg"), Entry("toc.xml")) + listOf(Url("dir1/im1.jpg")!!, Url("dir2/im2.jpg")!!, Url("toc.xml")!!) .pathCommonFirstComponent() ) } @@ -59,7 +39,7 @@ class ContainerEntryTest { fun `pathCommonFirstComponent is correct when there is only one file in the root`() { assertEquals( "im1.jpg", - listOf(Entry("im1.jpg")).pathCommonFirstComponent()?.name + listOf(Url("im1.jpg")!!).pathCommonFirstComponent()?.name ) } @@ -68,9 +48,9 @@ class ContainerEntryTest { assertEquals( "root", listOf( - Entry("root/im1.jpg"), - Entry("root/im2.jpg"), - Entry("root/xml/toc.xml") + Url("root/im1.jpg")!!, + Url("root/im2.jpg")!!, + Url("root/xml/toc.xml")!! ).pathCommonFirstComponent()?.name ) } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt index 15b6668b24..e5665d4bfe 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt @@ -9,6 +9,8 @@ package org.readium.r2.streamer.parser.epub +import java.io.File +import kotlin.test.* import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -16,11 +18,8 @@ import org.junit.runner.RunWith import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.assertSuccess -import org.readium.r2.shared.util.resource.DirectoryContainerFactory -import org.readium.r2.shared.util.resource.MediaTypeRetriever +import org.readium.r2.shared.util.resource.DirectoryContainer import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.flatMap -import org.readium.r2.shared.util.toAbsoluteUrl import org.readium.r2.streamer.readBlocking import org.robolectric.RobolectricTestRunner @@ -32,22 +31,19 @@ class EpubDeobfuscatorTest { private val deobfuscationDir = requireNotNull( EpubDeobfuscatorTest::class.java .getResource("deobfuscation") - ?.toAbsoluteUrl() + ?.path + ?.let { File(it) } ) private val container = runBlocking { - requireNotNull( - DirectoryContainerFactory(MediaTypeRetriever()).create(deobfuscationDir).getOrNull() - ) + DirectoryContainer(deobfuscationDir).assertSuccess() } - private val font = requireNotNull( - container.get(Url("cut-cut.woff")!!).readBlocking().getOrNull() - ) - - private fun deobfuscate(path: String, algorithm: String?): Resource { - val resource = container.get(Url(path)!!) + private val font = requireNotNull(container[Url("cut-cut.woff")!!]) + .readBlocking() + .assertSuccess() + private fun deobfuscate(url: Url, resource: Resource, algorithm: String?): Resource { val deobfuscator = EpubDeobfuscator(identifier) { if (resource.source == it) { algorithm?.let { @@ -58,13 +54,16 @@ class EpubDeobfuscatorTest { } } - return resource.flatMap(deobfuscator::transform) + return deobfuscator.transform(url, resource) } @Test fun testIdpfDeobfuscation() { + val url = Url("cut-cut.obf.woff")!! + val resource = assertNotNull(container[url]) val deobfuscatedRes = deobfuscate( - "/cut-cut.obf.woff", + url, + resource, "http://www.idpf.org/2008/embedding" ).readBlocking().getOrNull() assertThat(deobfuscatedRes).isEqualTo(font) @@ -73,8 +72,11 @@ class EpubDeobfuscatorTest { @Test fun testIdpfDeobfuscationWithRange() { runBlocking { + val url = Url("cut-cut.obf.woff")!! + val resource = assertNotNull(container[url]) val deobfuscatedRes = deobfuscate( - "/cut-cut.obf.woff", + url, + resource, "http://www.idpf.org/2008/embedding" ).read(20L until 40L).assertSuccess() assertThat(deobfuscatedRes).isEqualTo(font.copyOfRange(20, 40)) @@ -83,8 +85,11 @@ class EpubDeobfuscatorTest { @Test fun testAdobeDeobfuscation() { + val url = Url("cut-cut.adb.woff")!! + val resource = assertNotNull(container[url]) val deobfuscatedRes = deobfuscate( - "/cut-cut.adb.woff", + url, + resource, "http://ns.adobe.com/pdf/enc#RC" ).readBlocking().getOrNull() assertThat(deobfuscatedRes).isEqualTo(font) @@ -92,8 +97,11 @@ class EpubDeobfuscatorTest { @Test fun `a resource is passed through when the link doesn't contain encryption data`() { + val url = Url("cut-cut.woff")!! + val resource = assertNotNull(container[url]) val deobfuscatedRes = deobfuscate( - "/cut-cut.woff", + url, + resource, null ).readBlocking().getOrNull() assertThat(deobfuscatedRes).isEqualTo(font) @@ -101,8 +109,11 @@ class EpubDeobfuscatorTest { @Test fun `a resource is passed through when the algorithm is unknown`() { + val url = Url("cut-cut.woff")!! + val resource = assertNotNull(container[url]) val deobfuscatedRes = deobfuscate( - "/cut-cut.woff", + url, + resource, "unknown algorithm" ).readBlocking().getOrNull() assertThat(deobfuscatedRes).isEqualTo(font) diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt index 7bc707a426..099c325bcc 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt @@ -21,11 +21,12 @@ import org.readium.r2.shared.publication.presentation.Presentation 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.archive.ArchiveProperties +import org.readium.r2.shared.util.archive.archive +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ArchiveProperties -import org.readium.r2.shared.util.resource.Container +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceTry -import org.readium.r2.shared.util.resource.archive import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -42,7 +43,7 @@ class EpubPositionsServiceTest { fun `Positions from a {readingOrder} with one resource`() { val service = createService( readingOrder = listOf( - ReadingOrderItem(href = "res", length = 1, type = MediaType.XML) + ReadingOrderItem(href = Url("res")!!, length = 1, type = MediaType.XML) ) ) @@ -66,9 +67,9 @@ class EpubPositionsServiceTest { fun `Positions from a {readingOrder} with a few resources`() { val service = createService( readingOrder = listOf( - ReadingOrderItem("res", length = 1), - ReadingOrderItem("chap1", length = 2, MediaType.XML), - ReadingOrderItem("chap2", length = 2, MediaType.XHTML, title = "Chapter 2") + ReadingOrderItem(Url("res")!!, length = 1), + ReadingOrderItem(Url("chap1")!!, length = 2, MediaType.XML), + ReadingOrderItem(Url("chap2")!!, length = 2, MediaType.XHTML, title = "Chapter 2") ) ) @@ -111,8 +112,8 @@ class EpubPositionsServiceTest { fun `{type} fallbacks on text-html`() { val service = createService( readingOrder = listOf( - ReadingOrderItem("chap1", length = 1, layout = EpubLayout.REFLOWABLE), - ReadingOrderItem("chap2", length = 1, layout = EpubLayout.FIXED) + ReadingOrderItem(Url("chap1")!!, length = 1, layout = EpubLayout.REFLOWABLE), + ReadingOrderItem(Url("chap2")!!, length = 1, layout = EpubLayout.FIXED) ) ) @@ -146,9 +147,14 @@ class EpubPositionsServiceTest { val service = createService( layout = EpubLayout.FIXED, readingOrder = listOf( - ReadingOrderItem("res", length = 10000), - ReadingOrderItem("chap1", length = 20000, MediaType.XML), - ReadingOrderItem("chap2", length = 40000, MediaType.XHTML, title = "Chapter 2") + ReadingOrderItem(Url("res")!!, length = 10000), + ReadingOrderItem(Url("chap1")!!, length = 20000, MediaType.XML), + ReadingOrderItem( + Url("chap2")!!, + length = 40000, + MediaType.XHTML, + title = "Chapter 2" + ) ) ) @@ -192,11 +198,11 @@ class EpubPositionsServiceTest { val service = createService( layout = EpubLayout.REFLOWABLE, readingOrder = listOf( - ReadingOrderItem("chap1", length = 0), - ReadingOrderItem("chap2", length = 49, MediaType.XML), - ReadingOrderItem("chap3", length = 50, MediaType.XHTML, title = "Chapter 3"), - ReadingOrderItem("chap4", length = 51), - ReadingOrderItem("chap5", length = 120) + ReadingOrderItem(Url("chap1")!!, length = 0), + ReadingOrderItem(Url("chap2")!!, length = 49, MediaType.XML), + ReadingOrderItem(Url("chap3")!!, length = 50, MediaType.XHTML, title = "Chapter 3"), + ReadingOrderItem(Url("chap4")!!, length = 51), + ReadingOrderItem(Url("chap5")!!, length = 120) ), reflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength( pageLength = 50 @@ -289,7 +295,7 @@ class EpubPositionsServiceTest { val service = createService( layout = null, readingOrder = listOf( - ReadingOrderItem("chap1", length = 60) + ReadingOrderItem(Url("chap1")!!, length = 60) ), reflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength( pageLength = 50 @@ -326,9 +332,9 @@ class EpubPositionsServiceTest { val service = createService( layout = EpubLayout.FIXED, readingOrder = listOf( - ReadingOrderItem("chap1", length = 20000), - ReadingOrderItem("chap2", length = 60, layout = EpubLayout.REFLOWABLE), - ReadingOrderItem("chap3", length = 20000, layout = EpubLayout.FIXED) + ReadingOrderItem(Url("chap1")!!, length = 20000), + ReadingOrderItem(Url("chap2")!!, length = 60, layout = EpubLayout.REFLOWABLE), + ReadingOrderItem(Url("chap3")!!, length = 20000, layout = EpubLayout.FIXED) ), reflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength( pageLength = 50 @@ -383,8 +389,8 @@ class EpubPositionsServiceTest { val service = createService( layout = EpubLayout.REFLOWABLE, readingOrder = listOf( - ReadingOrderItem("chap1", length = 60, archiveEntryLength = 20L), - ReadingOrderItem("chap2", length = 60) + ReadingOrderItem(Url("chap1")!!, length = 60, archiveEntryLength = 20L), + ReadingOrderItem(Url("chap2")!!, length = 60) ), reflowableStrategy = EpubPositionsService.ReflowableStrategy.ArchiveEntryLength( pageLength = 50 @@ -434,8 +440,8 @@ class EpubPositionsServiceTest { val service = createService( layout = EpubLayout.REFLOWABLE, readingOrder = listOf( - ReadingOrderItem("chap1", length = 60, originalLength = 20L), - ReadingOrderItem("chap2", length = 60) + ReadingOrderItem(Url("chap1")!!, length = 60, originalLength = 20L), + ReadingOrderItem(Url("chap2")!!, length = 60) ), reflowableStrategy = EpubPositionsService.ReflowableStrategy.OriginalLength( pageLength = 50 @@ -484,25 +490,21 @@ class EpubPositionsServiceTest { ) ) = EpubPositionsService( readingOrder = readingOrder.map { it.link }, - container = object : Container { + container = object : Container { private fun find(relativePath: Url): ReadingOrderItem? = readingOrder.find { it.link.url() == relativePath } - override suspend fun entries(): Set? = null + override val entries: Set = readingOrder.map { it.href }.toSet() - override fun get(url: Url): Container.Entry { + override fun get(url: Url): Resource { val item = requireNotNull(find(url)) - return object : Container.Entry { - override val url: Url = url + return object : Resource { override val source: AbsoluteUrl? = null - override suspend fun mediaType(): ResourceTry = - Try.success(item.link.mediaType ?: MediaType.BINARY) - - override suspend fun properties(): ResourceTry = + override suspend fun properties(): ResourceTry = Try.success(item.resourceProperties) override suspend fun length() = Try.success(item.length) @@ -521,7 +523,7 @@ class EpubPositionsServiceTest { ) class ReadingOrderItem( - val href: String, + val href: Url, val length: Long, val type: MediaType? = null, val title: String? = null, @@ -530,7 +532,7 @@ class EpubPositionsServiceTest { val layout: EpubLayout? = null ) { val link: Link = Link( - href = Url(href)!!, + href = href, mediaType = type, title = title, properties = Properties( @@ -551,7 +553,7 @@ class EpubPositionsServiceTest { ) ) - val resourceProperties: Properties = Properties { + val resourceProperties: Resource.Properties = Resource.Properties { if (archiveEntryLength != null) { archive = ArchiveProperties( entryLength = archiveEntryLength, diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt index 5fca2b64a9..3c5eeef5b8 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/PackageDocumentTest.kt @@ -22,15 +22,14 @@ import org.readium.r2.shared.publication.epub.layout import org.readium.r2.shared.publication.presentation.* import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.xml.XmlParser import org.robolectric.RobolectricTestRunner fun parsePackageDocument(path: String): Manifest { val pub = PackageDocument::class.java.getResourceAsStream(path) ?.let { XmlParser().parse(it) } - ?.let { PackageDocument.parse(it, Url("OEBPS/content.opf")!!, MediaTypeRetriever()) } - ?.let { ManifestAdapter(it, mediaTypeRetriever = MediaTypeRetriever()) } + ?.let { PackageDocument.parse(it, Url("OEBPS/content.opf")!!) } + ?.let { ManifestAdapter(it) } ?.adapt() checkNotNull(pub) return pub diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index 2a2234fa63..205e711d4e 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -19,12 +19,15 @@ import org.junit.runner.RunWith import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.firstWithRel import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.asset.DefaultMediaTypeSniffer +import org.readium.r2.shared.util.asset.MediaTypeRetriever +import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.FileResource -import org.readium.r2.shared.util.resource.MediaTypeRetriever -import org.readium.r2.shared.util.resource.ResourceContainer +import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.shared.util.toUrl -import org.readium.r2.shared.util.zip.FileZipArchiveProvider +import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.readium.r2.streamer.parseBlocking import org.readium.r2.streamer.parser.PublicationParser import org.robolectric.RobolectricTestRunner @@ -32,15 +35,20 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class ImageParserTest { - private val parser = ImageParser() + private val mediaTypeRetriever = + MediaTypeRetriever( + DefaultMediaTypeSniffer(), + FormatRegistry(), + ZipArchiveFactory(), + null + ) + + private val parser = ImageParser(mediaTypeRetriever) private val cbzAsset = runBlocking { val file = fileForResource("futuristic_tales.cbz") val resource = FileResource(file, mediaType = MediaType.CBZ) - val archive = FileZipArchiveProvider(MediaTypeRetriever()).create( - resource, - password = null - ).getOrNull()!! + val archive = ZipArchiveFactory().create(MediaType.ZIP, resource).assertSuccess() PublicationParser.Asset(mediaType = MediaType.CBZ, archive) } @@ -49,7 +57,7 @@ class ImageParserTest { val resource = FileResource(file, mediaType = MediaType.JPEG) PublicationParser.Asset( mediaType = MediaType.JPEG, - ResourceContainer(file.toUrl(), resource) + SingleResourceContainer(file.toUrl(), resource) ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 4258b10c0d..b01676c06c 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -20,13 +20,13 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.asset.CompositeResourceFactory import org.readium.r2.shared.util.asset.ContentResourceFactory +import org.readium.r2.shared.util.asset.DefaultMediaTypeSniffer import org.readium.r2.shared.util.asset.FileResourceFactory import org.readium.r2.shared.util.asset.HttpResourceFactory +import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager import org.readium.r2.shared.util.http.DefaultHttpClient -import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer import org.readium.r2.shared.util.mediatype.FormatRegistry -import org.readium.r2.shared.util.resource.MediaTypeRetriever import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.readium.r2.streamer.PublicationFactory @@ -52,9 +52,7 @@ class Readium(context: Context) { context.contentResolver ) - val httpClient = DefaultHttpClient( - mediaTypeRetriever - ) + val httpClient = DefaultHttpClient() private val resourceFactory = CompositeResourceFactory( FileResourceFactory(), diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationUserError.kt index 1912bda044..295e6a1636 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationUserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationUserError.kt @@ -19,7 +19,7 @@ sealed class PublicationUserError( constructor(@StringRes userMessageId: Int) : this(UserError.Content(userMessageId), null) - class ReadError(cause: UserError) : + class ReadError(override val cause: ReadUserError) : PublicationUserError(cause.content, cause.cause) class UnsupportedScheme(val error: Error) : diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt index b2badcec06..7d9d0bc126 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt @@ -76,7 +76,7 @@ sealed class ReadUserError( HttpUnexpected(error) is HttpError.Timeout -> HttpConnectivity(error) - is HttpError.UnreachableHost -> + is HttpError.Unreachable -> HttpConnectivity(error) is HttpError.Response -> when (error.status) { From 268c3f521a5afc3c2c7664b613c768a7196524ae Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Tue, 28 Nov 2023 16:33:01 +0100 Subject: [PATCH 34/86] Cosmetic changes --- .../r2/shared/util/archive/ArchiveFactory.kt | 7 ++-- .../r2/shared/util/asset/AssetRetriever.kt | 14 ++++---- .../shared/util/asset/FileResourceFactory.kt | 3 ++ .../shared/util/asset/HttpResourceFactory.kt | 3 ++ .../shared/util/asset/MediaTypeRetriever.kt | 4 +-- .../r2/shared/util/asset/ResourceFactory.kt | 10 ++++++ .../asset/SimpleResourceMediaTypeRetriever.kt | 36 ++++++++++++++----- .../readium/r2/shared/util/data/Container.kt | 8 +++-- ...oviderError.kt => ContentResolverError.kt} | 11 +++--- .../r2/shared/util/data/FileSystemError.kt | 3 ++ .../readium/r2/shared/util/data/ReadError.kt | 13 ++++++- .../r2/shared/util/http/HttpContainer.kt | 3 +- .../r2/shared/util/http/HttpResource.kt | 2 -- .../r2/shared/util/http/HttpResponse.kt | 3 +- .../readium/r2/shared/util/http/HttpStatus.kt | 3 ++ .../shared/util/mediatype/FormatRegistry.kt | 7 ++++ .../shared/util/mediatype/MediaTypeSniffer.kt | 7 +++- .../shared/util/resource/ContentResource.kt | 8 ++--- .../shared/util/zip/FileZipArchiveProvider.kt | 5 +-- .../util/zip/StreamingZipArchiveProvider.kt | 5 +-- .../r2/testapp/domain/ReadUserError.kt | 12 +++---- 21 files changed, 114 insertions(+), 53 deletions(-) rename readium/shared/src/main/java/org/readium/r2/shared/util/data/{ContentProviderError.kt => ContentResolverError.kt} (75%) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt index 51c7b1a747..624e1b8519 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt @@ -14,7 +14,7 @@ import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource /** - * A factory to create a [ResourceContainer]s from archive [Readable]s. + * A factory to create [Container]s from archive [Resource]. */ public interface ArchiveFactory { @@ -24,8 +24,9 @@ public interface ArchiveFactory { ) : org.readium.r2.shared.util.Error { public class FormatNotSupported( + public val mediaType: MediaType, cause: org.readium.r2.shared.util.Error? = null - ) : Error("Resource is not supported.", cause) + ) : Error("Media type not supported.", cause) public class ReadError( override val cause: org.readium.r2.shared.util.data.ReadError @@ -63,6 +64,6 @@ public class CompositeArchiveFactory( ?.let { return Try.success(it) } } - return Try.failure(ArchiveFactory.Error.FormatNotSupported()) + return Try.failure(ArchiveFactory.Error.FormatNotSupported(mediaType)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 2960efd583..731050b40f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -20,14 +20,14 @@ import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.toUrl /** - * Retrieves an [Asset] instance providing reading access to the resource(s) of an asset stored at a - * given [Url]. + * Retrieves an [Asset] instance providing reading access to the resource(s) of an asset stored at + * a given [Url] as well as a canonical media type. */ public class AssetRetriever( private val mediaTypeRetriever: MediaTypeRetriever, private val resourceFactory: ResourceFactory, archiveFactory: ArchiveFactory, - formatRegistry: FormatRegistry = FormatRegistry() + formatRegistry: FormatRegistry ) { public sealed class Error( @@ -38,7 +38,7 @@ public class AssetRetriever( public class SchemeNotSupported( public val scheme: Url.Scheme, cause: org.readium.r2.shared.util.Error? = null - ) : Error("Scheme $scheme is not supported.", cause) + ) : Error("Url scheme $scheme is not supported.", cause) public class FormatNotSupported(cause: org.readium.r2.shared.util.Error) : Error("Asset format is not supported.", cause) @@ -51,7 +51,7 @@ public class AssetRetriever( SmartArchiveFactory(archiveFactory, formatRegistry) /** - * Retrieves an asset from a known media type. + * Retrieves an asset from an url and a known media type. */ public suspend fun retrieve( url: AbsoluteUrl, @@ -89,13 +89,13 @@ public class AssetRetriever( /* Sniff unknown assets */ /** - * Retrieves an asset from a local file. + * Retrieves an asset from an unknown local file. */ public suspend fun retrieve(file: File): Try = retrieve(file.toUrl()) /** - * Retrieves an asset from a [Url]. + * Retrieves an asset from an unknown [Url]. */ public suspend fun retrieve(url: AbsoluteUrl): Try { val resource = resourceFactory.create(url) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt index 47c382e447..239d1c9898 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt @@ -12,6 +12,9 @@ import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.FileResource import org.readium.r2.shared.util.resource.Resource +/** + * Creates [FileResource]s. + */ public class FileResourceFactory : ResourceFactory { override suspend fun create( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt index e19b48a55d..9653e5f805 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt @@ -13,6 +13,9 @@ import org.readium.r2.shared.util.http.HttpResource import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource +/** + * Creates [HttpResource]s. + */ public class HttpResourceFactory( private val httpClient: HttpClient ) : ResourceFactory { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt index ee634b01ea..8272010a42 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt @@ -81,8 +81,8 @@ public class MediaTypeRetriever( container: Container, hints: MediaTypeHints = MediaTypeHints() ): Try { - simpleResourceMediaTypeRetriever.retrieve(hints) - ?.let { Try.success(it) } + simpleResourceMediaTypeRetriever.retrieveSafe(hints) + .let { Try.success(it) } mediaTypeSniffer.sniffContainer(container) .onSuccess { return Try.success(it) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt index 824ac5f223..c67835e1ff 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt @@ -30,12 +30,22 @@ public interface ResourceFactory { ) : Error("Url scheme $scheme is not supported.", cause) } + /** + * Creates a [Resource] to access [url]. + * + * @param url The url the resource will access. + * @param mediaType media type of the resource if known. + */ public suspend fun create( url: AbsoluteUrl, mediaType: MediaType? = null ): Try } +/** + * A composite [ResourceFactory] which tries several factories until it finds one which supports + * the url scheme. + */ public class CompositeResourceFactory( private val factories: List ) : ResourceFactory { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt index 8721a3feff..3a8e87d8df 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt @@ -11,6 +11,7 @@ import android.provider.MediaStore import java.io.File import org.readium.r2.shared.extensions.queryProjection import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType @@ -22,6 +23,9 @@ import org.readium.r2.shared.util.resource.invoke import org.readium.r2.shared.util.toUri import org.readium.r2.shared.util.tryRecover +/** + * A [MediaTypeRetriever] which does not open archive resources. + */ internal class SimpleResourceMediaTypeRetriever( private val mediaTypeSniffer: MediaTypeSniffer, private val contentResolver: ContentResolver?, @@ -29,14 +33,24 @@ internal class SimpleResourceMediaTypeRetriever( ) { /** - * Retrieves a canonical [MediaType] for the provided media type and file extension [hints]. + * Retrieves a canonical [MediaType] for the provided media type [hints]. + * + * Does not recognize media types and file extensions for too generic types. */ - fun retrieve(hints: MediaTypeHints): MediaType? = + fun retrieveSafe(hints: MediaTypeHints): Try = retrieveUnsafe(hints) - .getOrNull() - ?.takeUnless { formatRegistry.isSuperType(it) } + .flatMap { + if (formatRegistry.isSuperType(it)) { + Try.failure(MediaTypeSnifferError.NotRecognized) + } else { + Try.success(it) + } + } - internal fun retrieveUnsafe(hints: MediaTypeHints): Try = + /** + * Retrieves a [MediaType] as much canonical as possible without accessing the content. + */ + fun retrieveUnsafe(hints: MediaTypeHints): Try = mediaTypeSniffer.sniffHints(hints) .tryRecover { hints.mediaTypes.firstOrNull() @@ -44,12 +58,16 @@ internal class SimpleResourceMediaTypeRetriever( ?: Try.failure(MediaTypeSnifferError.NotRecognized) } + /** + * Retrieves a [MediaType] for [resource] using [hints] added to those embedded in [resource] + * and reading content if necessary. + */ suspend fun retrieve(resource: Resource, hints: MediaTypeHints): Try { val properties = resource.properties() .getOrElse { return Try.failure(MediaTypeSnifferError.Read(it)) } - retrieve(MediaTypeHints(properties) + hints) - ?.also { return Try.success(it) } + retrieveSafe(MediaTypeHints(properties) + hints) + .onSuccess { return Try.success(it) } if (contentResolver != null) { resource.source @@ -64,8 +82,8 @@ internal class SimpleResourceMediaTypeRetriever( ?.let { filename -> File(filename).extension } ) - retrieve(contentHints) - ?.let { return Try.success(it) } + retrieveSafe(contentHints) + .onSuccess { return Try.success(it) } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index 97ba79950d..70a5180243 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -10,13 +10,15 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Resource /** - * A container provides access to a list of [Resource] entries. + * A container provides access to a list of [Readable] entries. */ public interface Container : Iterable, SuspendingCloseable { + /** + * Media type of the archive the container offers access to if any. + */ public val archiveMediaType: MediaType? get() = null /** @@ -38,7 +40,7 @@ public interface Container : Iterable, SuspendingCloseabl public operator fun get(url: Url): E? } -/** A [Container] providing no resources at all. */ +/** A [Container] providing no entries at all. */ public class EmptyContainer : Container { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentResolverError.kt similarity index 75% rename from readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentResolverError.kt index 372b48c3d3..b81789dd09 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentProviderError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentResolverError.kt @@ -9,28 +9,31 @@ package org.readium.r2.shared.util.data import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ThrowableError -public sealed class ContentProviderError( +/** + * Errors wrapping Android Content Provider errors. + */ +public sealed class ContentResolverError( override val message: String, override val cause: Error? = null ) : AccessError { public class FileNotFound( cause: Error? - ) : ContentProviderError("File not found.", cause) { + ) : ContentResolverError("File not found.", cause) { public constructor(exception: Exception) : this(ThrowableError(exception)) } public class NotAvailable( cause: Error? - ) : ContentProviderError("Content Provider recently crashed.", cause) { + ) : ContentResolverError("Content Provider recently crashed.", cause) { public constructor(exception: Exception) : this(ThrowableError(exception)) } public class IO( override val cause: Error - ) : ContentProviderError("An IO error occurred.", cause) { + ) : ContentResolverError("An IO error occurred.", cause) { public constructor(exception: Exception) : this(ThrowableError(exception)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt index 1efb14df9c..e0150c6466 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt @@ -9,6 +9,9 @@ package org.readium.r2.shared.util.data import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ThrowableError +/** + * Errors wrapping file system exceptions. + */ public sealed class FileSystemError( override val message: String, override val cause: Error? = null diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt index 56203ded84..42ef0256d8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt @@ -13,7 +13,7 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.ThrowableError /** - * Errors occurring while accessing a resource. + * Errors occurring while reading a resource. */ public sealed class ReadError( override val message: String, @@ -43,8 +43,19 @@ public sealed class ReadError( } } +/** + * Marker interface for source-specific access errors. + * + * At the moment, [AccessError]s constructed by the toolkit can be either a [FileSystemError], + * a [ContentResolverError] or an HttpError. + */ public interface AccessError : Error +/** + * An [IOException] wrapping a [ReadError]. + * + * This is meant to be used in contexts where [IOException] are expected. + */ public class ReadException( public val error: ReadError ) : IOException(error.message, ErrorException(error)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt index 1f5843ce1e..3df0449dad 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpContainer.kt @@ -17,8 +17,9 @@ import org.readium.r2.shared.util.resource.Resource * Since this container is used when doing progressive download streaming (e.g. audiobook), the HTTP * byte range requests are open-ended and reused. This helps to avoid issuing too many requests. * - * @param client HTTP client used to perform HTTP requests. * @param baseUrl Base URL from which relative URLs are served. + * @param entries Entries of this container as Urls absolute or relative to [baseUrl]. + * @param client HTTP client used to perform HTTP requests. */ public class HttpContainer( private val baseUrl: Url? = null, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index bd1bc9abf4..e821937202 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -10,7 +10,6 @@ import java.io.IOException import java.io.InputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.extensions.read import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl @@ -24,7 +23,6 @@ import org.readium.r2.shared.util.resource.filename import org.readium.r2.shared.util.resource.mediaType /** Provides access to an external URL through HTTP. */ -@OptIn(ExperimentalReadiumApi::class) public class HttpResource( override val source: AbsoluteUrl, private val client: HttpClient, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt index c57a92afd6..7cb4801e66 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt @@ -16,8 +16,7 @@ import org.readium.r2.shared.util.mediatype.MediaType * @param url Final URL of the response. * @param statusCode Response status code. * @param headers HTTP response headers, indexed by their name. - * @param mediaType Media type sniffed from the `Content-Type` header and response body. Falls back - * on `application/octet-stream`. + * @param mediaType Media type from the `Content-Type` header. */ public data class HttpResponse( val request: HttpRequest, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt index c21dd89e33..e71790cf16 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt @@ -7,6 +7,9 @@ package org.readium.r2.shared.util.http @JvmInline +/** + * Status code of an HTTP response. + */ public value class HttpStatus( public val code: Int ) : Comparable { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt index c44b9678f3..66685dc832 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt @@ -83,9 +83,16 @@ public class FormatRegistry( public fun fileExtension(mediaType: MediaType): String? = fileExtensions[mediaType] + /** + * Returns the super type of the given [mediaType], if any. + */ public fun superType(mediaType: MediaType): MediaType? = superTypes[mediaType] + /** + * Returns if [mediaType] is a generic type that could be used instead of more specific + * media types. + */ public fun isSuperType(mediaType: MediaType): Boolean = superTypes.values.any { it.matches(mediaType) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index 9738803416..6fe70c7163 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -674,6 +674,11 @@ public object LpfMediaTypeSniffer : MediaTypeSniffer { } } +/** + * Sniffs a RAR archive. + * + * At the moment, only hints are supported. + */ public object RarMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try { @@ -691,7 +696,7 @@ public object RarMediaTypeSniffer : MediaTypeSniffer { } /** - * Sniffs a simple Archive-based format, like Comic Book Archive or Zipped Audio Book. + * Sniffs a simple Archive-based publication format, like Comic Book Archive or Zipped Audio Book. * * Reference: https://wiki.mobileread.com/wiki/CBR_and_CBZ */ diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt index d31cda5d44..1b1f5d6940 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt @@ -19,7 +19,7 @@ import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.ContentProviderError +import org.readium.r2.shared.util.data.ContentResolverError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.mediatype.MediaType @@ -119,7 +119,7 @@ public class ContentResource( val stream = contentResolver.openInputStream(uri) ?: return Try.failure( ReadError.Access( - ContentProviderError.NotAvailable( + ContentResolverError.NotAvailable( MessageError("Content provider recently crashed.") ) ) @@ -134,9 +134,9 @@ public class ContentResource( try { success(closure()) } catch (e: FileNotFoundException) { - failure(ReadError.Access(ContentProviderError.FileNotFound(e))) + failure(ReadError.Access(ContentResolverError.FileNotFound(e))) } catch (e: IOException) { - failure(ReadError.Access(ContentProviderError.IO(e))) + failure(ReadError.Access(ContentResolverError.IO(e))) } catch (e: OutOfMemoryError) { // We don't want to catch any Error, only OOM. failure(ReadError.OutOfMemory(e)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index 1cb3dac847..5fcb5e8fe7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -13,7 +13,6 @@ import java.util.zip.ZipException import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.data.Container @@ -58,9 +57,7 @@ internal class FileZipArchiveProvider { ): Try, ArchiveFactory.Error> { if (mediaType != MediaType.ZIP) { return Try.failure( - ArchiveFactory.Error.FormatNotSupported( - MessageError("Archive type not supported") - ) + ArchiveFactory.Error.FormatNotSupported(mediaType) ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 9180e07022..2c6154e708 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -12,7 +12,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.unwrapInstance import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.data.Container @@ -53,9 +52,7 @@ internal class StreamingZipArchiveProvider { ): Try, ArchiveFactory.Error> { if (mediaType != MediaType.ZIP) { return Try.failure( - ArchiveFactory.Error.FormatNotSupported( - MessageError("Archive type not supported") - ) + ArchiveFactory.Error.FormatNotSupported(mediaType) ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt index 7d9d0bc126..be750380b7 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt @@ -8,7 +8,7 @@ package org.readium.r2.testapp.domain import androidx.annotation.StringRes import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.data.ContentProviderError +import org.readium.r2.shared.util.data.ContentResolverError import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.http.HttpError @@ -58,7 +58,7 @@ sealed class ReadUserError( when (val cause = error.cause) { is HttpError -> ReadUserError(cause) is FileSystemError -> ReadUserError(cause) - is ContentProviderError -> ReadUserError(cause) + is ContentResolverError -> ReadUserError(cause) else -> Unexpected(cause) } is ReadError.Decoding -> InvalidPublication(error) @@ -93,11 +93,11 @@ sealed class ReadUserError( is FileSystemError.NotFound -> FsNotFound(error) } - private operator fun invoke(error: ContentProviderError): ReadUserError = + private operator fun invoke(error: ContentResolverError): ReadUserError = when (error) { - is ContentProviderError.FileNotFound -> FsNotFound(error) - is ContentProviderError.IO -> FsUnexpected(error) - is ContentProviderError.NotAvailable -> FsUnexpected(error) + is ContentResolverError.FileNotFound -> FsNotFound(error) + is ContentResolverError.IO -> FsUnexpected(error) + is ContentResolverError.NotAvailable -> FsUnexpected(error) } } } From 8ec36a82fdbe8021765b477ccb2280d888c36031 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 29 Nov 2023 13:59:28 +0100 Subject: [PATCH 35/86] Cosmetic changes --- .../readium/r2/lcp/LcpContentProtection.kt | 10 +-- .../readium/r2/lcp/service/NetworkService.kt | 2 +- .../AdeptFallbackContentProtection.kt | 10 +-- .../protection/ContentProtection.kt | 4 +- .../LcpFallbackContentProtection.kt | 10 +-- .../r2/shared/util/asset/AssetRetriever.kt | 6 +- .../shared/util/asset/MediaTypeRetriever.kt | 8 +- .../asset/SimpleResourceMediaTypeRetriever.kt | 37 +++------ .../readium/r2/shared/util/data/Decoding.kt | 42 +++++----- .../r2/shared/util/http/HttpResource.kt | 2 + .../shared/util/mediatype/MediaTypeSniffer.kt | 78 +++++++++---------- .../shared/util/resource/ContentResource.kt | 37 ++++++--- .../r2/shared/util/resource/FileProperties.kt | 10 ++- .../content/ResourceContentExtractor.kt | 6 +- .../shared/util/zip/FileZipArchiveProvider.kt | 4 +- .../util/zip/StreamingZipArchiveProvider.kt | 2 +- .../readium/r2/streamer/ParserAssetFactory.kt | 6 +- .../readium/r2/streamer/PublicationFactory.kt | 2 +- .../r2/streamer/parser/audio/AudioParser.kt | 2 +- .../r2/streamer/parser/epub/EpubParser.kt | 8 +- .../r2/streamer/parser/image/ImageParser.kt | 2 +- .../parser/readium/ReadiumWebPubParser.kt | 6 +- .../r2/testapp/domain/PublicationError.kt | 2 +- 23 files changed, 147 insertions(+), 149 deletions(-) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index 3c78dca446..15185c24ff 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -112,7 +112,7 @@ internal class LcpContentProtection( LicenseDocument(it) } catch (e: Exception) { return Try.failure( - ContentProtection.Error.ReadError( + ContentProtection.Error.Reading( ReadError.Decoding( MessageError( "Failed to read the LCP license document", @@ -125,14 +125,14 @@ internal class LcpContentProtection( } .getOrElse { return Try.failure( - ContentProtection.Error.ReadError(it) + ContentProtection.Error.Reading(it) ) } val link = licenseDoc.publicationLink val url = (link.url() as? AbsoluteUrl) ?: return Try.failure( - ContentProtection.Error.ReadError( + ContentProtection.Error.Reading( ReadError.Decoding( MessageError( "The LCP license document does not contain a valid link to the publication" @@ -174,8 +174,8 @@ internal class LcpContentProtection( when (this) { is AssetRetriever.Error.FormatNotSupported -> ContentProtection.Error.UnsupportedAsset(this) - is AssetRetriever.Error.ReadError -> - ContentProtection.Error.ReadError(cause) + is AssetRetriever.Error.Reading -> + ContentProtection.Error.Reading(cause) is AssetRetriever.Error.SchemeNotSupported -> ContentProtection.Error.UnsupportedAsset(this) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt index 31808ea74b..8b36e4f9a0 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt @@ -149,7 +149,7 @@ internal class NetworkService( when (it) { is MediaTypeSnifferError.NotRecognized -> MediaType.BINARY - is MediaTypeSnifferError.Read -> + is MediaTypeSnifferError.Reading -> throw ReadException(it.cause) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt index fb6e535313..ccb796edc6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt @@ -13,7 +13,7 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.readAsXml import org.readium.r2.shared.util.getOrElse @@ -70,9 +70,9 @@ public class AdeptFallbackContentProtection : ContentProtection { ?.readAsXml() ?.getOrElse { when (it) { - is DecoderError.Decoding -> + is DecodeError.Decoding -> return Try.success(false) - is DecoderError.Read -> + is DecodeError.Reading -> return Try.failure(it.cause) } }?.get("EncryptedData", EpubEncryption.ENC) @@ -85,9 +85,9 @@ public class AdeptFallbackContentProtection : ContentProtection { ?.readAsXml() ?.getOrElse { when (it) { - is DecoderError.Decoding -> + is DecodeError.Decoding -> return Try.success(false) - is DecoderError.Read -> + is DecodeError.Reading -> return Try.failure(it.cause) } }?.takeIf { it.namespace == "http://ns.adobe.com/adept" } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index daa52c27e4..218e4043eb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -37,8 +37,8 @@ public interface ContentProtection { override val cause: org.readium.r2.shared.util.Error? ) : org.readium.r2.shared.util.Error { - public class ReadError( - override val cause: org.readium.r2.shared.util.data.ReadError + public class Reading( + override val cause: ReadError ) : Error("An error occurred while trying to read asset.", cause) public class UnsupportedAsset( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index 63f3874bca..5d35000170 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -14,7 +14,7 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.readAsRwpm import org.readium.r2.shared.util.data.readAsXml @@ -95,9 +95,9 @@ public class LcpFallbackContentProtection : ContentProtection { ?.readAsRwpm() ?.getOrElse { when (it) { - is DecoderError.Read -> + is DecodeError.Reading -> return Try.failure(ReadError.Decoding(it)) - is DecoderError.Decoding -> + is DecodeError.Decoding -> return Try.success(false) } } @@ -116,9 +116,9 @@ public class LcpFallbackContentProtection : ContentProtection { ?.readAsXml() ?.getOrElse { when (it) { - is DecoderError.Read -> + is DecodeError.Reading -> return Try.failure(ReadError.Decoding(it.cause.cause)) - is DecoderError.Decoding -> + is DecodeError.Decoding -> return Try.failure(ReadError.Decoding(it.cause)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 731050b40f..5f2f22d565 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -43,7 +43,7 @@ public class AssetRetriever( public class FormatNotSupported(cause: org.readium.r2.shared.util.Error) : Error("Asset format is not supported.", cause) - public class ReadError(override val cause: org.readium.r2.shared.util.data.ReadError) : + public class Reading(override val cause: org.readium.r2.shared.util.data.ReadError) : Error("An error occurred when trying to read asset.", cause) } @@ -64,7 +64,7 @@ public class AssetRetriever( .getOrElse { return when (it) { is ArchiveFactory.Error.ReadError -> - Try.failure(Error.ReadError(it.cause)) + Try.failure(Error.Reading(it.cause)) is ArchiveFactory.Error.FormatNotSupported -> Try.success(Asset.Resource(mediaType, resource)) } @@ -121,7 +121,7 @@ public class AssetRetriever( .getOrElse { when (it) { is ArchiveFactory.Error.ReadError -> - return Try.failure(Error.ReadError(it.cause)) + return Try.failure(Error.Reading(it.cause)) is ArchiveFactory.Error.FormatNotSupported -> return Try.success(Asset.Resource(mediaType, resource)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt index 8272010a42..6514d2a5ec 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt @@ -6,7 +6,6 @@ package org.readium.r2.shared.util.asset -import android.content.ContentResolver import java.io.File import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.archive.ArchiveFactory @@ -33,12 +32,11 @@ import org.readium.r2.shared.util.use public class MediaTypeRetriever( private val mediaTypeSniffer: MediaTypeSniffer, formatRegistry: FormatRegistry, - archiveFactory: ArchiveFactory, - contentResolver: ContentResolver? + archiveFactory: ArchiveFactory ) { private val simpleResourceMediaTypeRetriever: SimpleResourceMediaTypeRetriever = - SimpleResourceMediaTypeRetriever(mediaTypeSniffer, contentResolver, formatRegistry) + SimpleResourceMediaTypeRetriever(mediaTypeSniffer, formatRegistry) private val archiveFactory: ArchiveFactory = SmartArchiveFactory(archiveFactory, formatRegistry) @@ -116,7 +114,7 @@ public class MediaTypeRetriever( .getOrElse { when (it) { is ArchiveFactory.Error.ReadError -> - return Try.failure(MediaTypeSnifferError.Read(it.cause)) + return Try.failure(MediaTypeSnifferError.Reading(it.cause)) is ArchiveFactory.Error.FormatNotSupported -> return Try.success(resourceMediaType) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt index 3a8e87d8df..aef36680b9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt @@ -6,10 +6,6 @@ package org.readium.r2.shared.util.asset -import android.content.ContentResolver -import android.provider.MediaStore -import java.io.File -import org.readium.r2.shared.extensions.queryProjection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse @@ -19,8 +15,8 @@ import org.readium.r2.shared.util.mediatype.MediaTypeHints import org.readium.r2.shared.util.mediatype.MediaTypeSniffer import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.invoke -import org.readium.r2.shared.util.toUri +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.resource.mediaType import org.readium.r2.shared.util.tryRecover /** @@ -28,7 +24,6 @@ import org.readium.r2.shared.util.tryRecover */ internal class SimpleResourceMediaTypeRetriever( private val mediaTypeSniffer: MediaTypeSniffer, - private val contentResolver: ContentResolver?, private val formatRegistry: FormatRegistry ) { @@ -64,28 +59,16 @@ internal class SimpleResourceMediaTypeRetriever( */ suspend fun retrieve(resource: Resource, hints: MediaTypeHints): Try { val properties = resource.properties() - .getOrElse { return Try.failure(MediaTypeSnifferError.Read(it)) } + .getOrElse { return Try.failure(MediaTypeSnifferError.Reading(it)) } - retrieveSafe(MediaTypeHints(properties) + hints) - .onSuccess { return Try.success(it) } - - if (contentResolver != null) { - resource.source - ?.takeIf { it.isContent } - ?.let { url -> - val contentHints = MediaTypeHints( - mediaType = contentResolver.getType(url.toUri()) - ?.let { MediaType(it) } - ?.takeUnless { it.matches(MediaType.BINARY) }, - fileExtension = contentResolver - .queryProjection(url.uri, MediaStore.MediaColumns.DISPLAY_NAME) - ?.let { filename -> File(filename).extension } - ) + val embeddedHints = MediaTypeHints( + mediaType = properties.mediaType, + fileExtension = properties.filename + ?.substringAfterLast(".", "") + ) - retrieveSafe(contentHints) - .onSuccess { return Try.success(it) } - } - } + retrieveSafe(embeddedHints + hints) + .onSuccess { return Try.success(it) } mediaTypeSniffer.sniffBlob(resource) .onSuccess { return Try.success(it) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index b0eacfa5e1..c3d0a8ddf0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -23,23 +23,23 @@ import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.xml.ElementNode import org.readium.r2.shared.util.xml.XmlParser -public sealed class DecoderError( +public sealed class DecodeError( override val message: String ) : Error { - public class Read( + public class Reading( override val cause: ReadError - ) : DecoderError("Reading error") + ) : DecodeError("Reading error") public class Decoding( override val cause: Error? - ) : DecoderError("Decoding Error") + ) : DecodeError("Decoding Error") } internal suspend fun Try.decode( block: (value: S) -> R, wrapError: (Exception) -> Error -): Try = +): Try = when (this) { is Try.Success -> try { @@ -47,18 +47,18 @@ internal suspend fun Try.decode( Try.success(block(value)) } } catch (e: Exception) { - Try.failure(DecoderError.Decoding(wrapError(e))) + Try.failure(DecodeError.Decoding(wrapError(e))) } catch (e: OutOfMemoryError) { - Try.failure(DecoderError.Read(ReadError.OutOfMemory(e))) + Try.failure(DecodeError.Reading(ReadError.OutOfMemory(e))) } is Try.Failure -> - Try.failure(DecoderError.Read(value)) + Try.failure(DecodeError.Reading(value)) } -internal suspend fun Try.decodeMap( +internal suspend fun Try.decodeMap( block: (value: S) -> R, wrapError: (Exception) -> Error -): Try = +): Try = when (this) { is Try.Success -> try { @@ -66,9 +66,9 @@ internal suspend fun Try.decodeMap( Try.success(block(value)) } } catch (e: Exception) { - Try.failure(DecoderError.Decoding(wrapError(e))) + Try.failure(DecodeError.Decoding(wrapError(e))) } catch (e: OutOfMemoryError) { - Try.failure(DecoderError.Read(ReadError.OutOfMemory(e))) + Try.failure(DecodeError.Reading(ReadError.OutOfMemory(e))) } is Try.Failure -> Try.failure(value) @@ -82,14 +82,14 @@ internal suspend fun Try.decodeMap( */ public suspend fun Readable.readAsString( charset: Charset = Charsets.UTF_8 -): Try = +): Try = read().decode( { String(it, charset = charset) }, { MessageError("Content is not a valid $charset string.", ThrowableError(it)) } ) /** Content as an XML document. */ -public suspend fun Readable.readAsXml(): Try = +public suspend fun Readable.readAsXml(): Try = read().decode( { XmlParser().parse(ByteArrayInputStream(it)) }, { MessageError("Content is not a valid XML document.", ThrowableError(it)) } @@ -98,19 +98,19 @@ public suspend fun Readable.readAsXml(): Try = /** * Content parsed from JSON. */ -public suspend fun Readable.readAsJson(): Try = +public suspend fun Readable.readAsJson(): Try = readAsString().decodeMap( { JSONObject(it) }, { MessageError("Content is not valid JSON.", ThrowableError(it)) } ) /** Readium Web Publication Manifest parsed from the content. */ -public suspend fun Readable.readAsRwpm(): Try = +public suspend fun Readable.readAsRwpm(): Try = readAsJson().flatMap { json -> Manifest.fromJSON(json) ?.let { Try.success(it) } ?: Try.failure( - DecoderError.Decoding( + DecodeError.Decoding( MessageError("Content is not a valid RWPM.") ) ) @@ -119,14 +119,14 @@ public suspend fun Readable.readAsRwpm(): Try = /** * Reads the full content as a [Bitmap]. */ -public suspend fun Readable.readAsBitmap(): Try = +public suspend fun Readable.readAsBitmap(): Try = read() - .mapFailure { DecoderError.Read(it) } + .mapFailure { DecodeError.Reading(it) } .flatMap { bytes -> BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?.let { Try.success(it) } ?: Try.failure( - DecoderError.Decoding( + DecodeError.Decoding( MessageError("Could not decode resource as a bitmap.") ) ) @@ -137,7 +137,7 @@ public suspend fun Readable.readAsBitmap(): Try = */ public suspend fun Readable.containsJsonKeys( vararg keys: String -): Try { +): Try { val json = readAsJson() .getOrElse { return Try.failure(it) } return Try.success(json.keys().asSequence().toSet().containsAll(keys.toList())) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index e821937202..bd1bc9abf4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -10,6 +10,7 @@ import java.io.IOException import java.io.InputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.extensions.read import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl @@ -23,6 +24,7 @@ import org.readium.r2.shared.util.resource.filename import org.readium.r2.shared.util.resource.mediaType /** Provides access to an external URL through HTTP. */ +@OptIn(ExperimentalReadiumApi::class) public class HttpResource( override val source: AbsoluteUrl, private val client: HttpClient, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index 6fe70c7163..da5f2e94a7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -23,7 +23,7 @@ import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.data.ReadableInputStream @@ -42,7 +42,7 @@ public sealed class MediaTypeSnifferError( public data object NotRecognized : MediaTypeSnifferError("Media type of resource could not be inferred.", null) - public data class Read(override val cause: ReadError) : + public data class Reading(override val cause: ReadError) : MediaTypeSnifferError("An error occurred while trying to read content.", cause) } public interface HintMediaTypeSniffer { @@ -172,11 +172,11 @@ public object XhtmlMediaTypeSniffer : MediaTypeSniffer { readable.readAsXml() .getOrElse { when (it) { - is DecoderError.Read -> + is DecodeError.Reading -> return Try.failure( - MediaTypeSnifferError.Read(it.cause) + MediaTypeSnifferError.Reading(it.cause) ) - is DecoderError.Decoding -> + is DecodeError.Decoding -> null } } @@ -213,9 +213,9 @@ public object HtmlMediaTypeSniffer : MediaTypeSniffer { readable.readAsXml() .getOrElse { when (it) { - is DecoderError.Read -> - return Try.failure(MediaTypeSnifferError.Read(it.cause)) - is DecoderError.Decoding -> + is DecodeError.Reading -> + return Try.failure(MediaTypeSnifferError.Reading(it.cause)) + is DecodeError.Decoding -> null } } @@ -225,10 +225,10 @@ public object HtmlMediaTypeSniffer : MediaTypeSniffer { readable.readAsString() .getOrElse { when (it) { - is DecoderError.Read -> - return Try.failure(MediaTypeSnifferError.Read(it.cause)) + is DecodeError.Reading -> + return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - is DecoderError.Decoding -> + is DecodeError.Decoding -> null } } @@ -285,9 +285,9 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { readable.readAsXml() .getOrElse { when (it) { - is DecoderError.Read -> - return Try.failure(MediaTypeSnifferError.Read(it.cause)) - is DecoderError.Decoding -> + is DecodeError.Reading -> + return Try.failure(MediaTypeSnifferError.Reading(it.cause)) + is DecodeError.Decoding -> null } } @@ -304,9 +304,9 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { readable.readAsRwpm() .getOrElse { when (it) { - is DecoderError.Read -> - return Try.failure(MediaTypeSnifferError.Read(it.cause)) - is DecoderError.Decoding -> + is DecodeError.Reading -> + return Try.failure(MediaTypeSnifferError.Reading(it.cause)) + is DecodeError.Decoding -> null } } @@ -336,10 +336,10 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { readable.containsJsonKeys("id", "title", "authentication") .getOrElse { when (it) { - is DecoderError.Read -> - return Try.failure(MediaTypeSnifferError.Read(it.cause)) + is DecodeError.Reading -> + return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - is DecoderError.Decoding -> + is DecodeError.Decoding -> null } } @@ -371,10 +371,10 @@ public object LcpLicenseMediaTypeSniffer : MediaTypeSniffer { readable.containsJsonKeys("id", "issued", "provider", "encryption") .getOrElse { when (it) { - is DecoderError.Read -> - return Try.failure(MediaTypeSnifferError.Read(it.cause)) + is DecodeError.Reading -> + return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - is DecoderError.Decoding -> + is DecodeError.Decoding -> null } } @@ -467,10 +467,10 @@ public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { readable.readAsRwpm() .getOrElse { when (it) { - is DecoderError.Read -> - return Try.failure(MediaTypeSnifferError.Read(it.cause)) + is DecodeError.Reading -> + return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - is DecoderError.Decoding -> + is DecodeError.Decoding -> null } } @@ -537,7 +537,7 @@ public object WebPubMediaTypeSniffer : MediaTypeSniffer { container[RelativeUrl("manifest.json")!!] ?.read() ?.getOrElse { error -> - return Try.failure(MediaTypeSnifferError.Read(error)) + return Try.failure(MediaTypeSnifferError.Reading(error)) } ?.let { tryOrNull { Manifest.fromJSON(JSONObject(String(it))) } } ?: return Try.failure(MediaTypeSnifferError.NotRecognized) @@ -576,10 +576,10 @@ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { val string = readable.readAsString() .getOrElse { when (it) { - is DecoderError.Read -> - return Try.failure(MediaTypeSnifferError.Read(it.cause)) + is DecodeError.Reading -> + return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - is DecoderError.Decoding -> + is DecodeError.Decoding -> null } } ?: "" @@ -616,10 +616,10 @@ public object EpubMediaTypeSniffer : MediaTypeSniffer { ?.readAsString(charset = Charsets.US_ASCII) ?.getOrElse { error -> when (error) { - is DecoderError.Decoding -> + is DecodeError.Decoding -> null - is DecoderError.Read -> - return Try.failure(MediaTypeSnifferError.Read(error.cause)) + is DecodeError.Reading -> + return Try.failure(MediaTypeSnifferError.Reading(error.cause)) } }?.trim() if (mimetype == "application/epub+zip") { @@ -658,7 +658,7 @@ public object LpfMediaTypeSniffer : MediaTypeSniffer { container[RelativeUrl("publication.json")!!] ?.read() ?.getOrElse { error -> - return Try.failure(MediaTypeSnifferError.Read(error)) + return Try.failure(MediaTypeSnifferError.Reading(error)) } ?.let { tryOrNull { String(it) } } ?.let { manifest -> @@ -829,7 +829,7 @@ public object PdfMediaTypeSniffer : MediaTypeSniffer { override suspend fun sniffBlob(readable: Readable): Try { readable.read(0L until 5L) .getOrElse { error -> - return Try.failure(MediaTypeSnifferError.Read(error)) + return Try.failure(MediaTypeSnifferError.Reading(error)) } .let { tryOrNull { it.toString(Charsets.UTF_8) } } .takeIf { it == "%PDF-" } @@ -857,10 +857,10 @@ public object JsonMediaTypeSniffer : MediaTypeSniffer { readable.readAsJson() .getOrElse { when (it) { - is DecoderError.Read -> - return Try.failure(MediaTypeSnifferError.Read(it.cause)) + is DecodeError.Reading -> + return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - is DecoderError.Decoding -> + is DecodeError.Decoding -> null } } @@ -904,7 +904,7 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { e.asInstance(SystemSnifferException::class.java) ?.let { return Try.failure( - MediaTypeSnifferError.Read(it.error) + MediaTypeSnifferError.Reading(it.error) ) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt index 1b1f5d6940..d496473efc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt @@ -36,17 +36,7 @@ public class ContentResource( private lateinit var _length: Try - private val filename = - contentResolver.queryProjection(uri, MediaStore.MediaColumns.DISPLAY_NAME) - - private val properties = - Resource.Properties( - Resource.Properties.Builder() - .also { - it.filename = filename - it.mediaType = mediaType - } - ) + private lateinit var _properties: Try override val source: AbsoluteUrl? = uri.toUrl() as? AbsoluteUrl @@ -54,7 +44,30 @@ public class ContentResource( } override suspend fun properties(): Try { - return Try.success(properties) + if (::_properties.isInitialized) { + return _properties + } + + val filename = + contentResolver.queryProjection(uri, MediaStore.MediaColumns.DISPLAY_NAME) + + val mediaType: MediaType? = this.mediaType + ?: contentResolver.getType(uri) + ?.let { MediaType(it) } + ?.takeUnless { it.matches(MediaType.BINARY) } + + val properties = + Resource.Properties( + Resource.Properties.Builder() + .also { + it.filename = filename + it.mediaType = mediaType + } + ) + + _properties = Try.success(properties) + + return _properties } override suspend fun read(range: LongRange?): Try { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt index 0553b27899..de758f5f87 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt @@ -1,7 +1,12 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints private const val FILENAME_KEY = "filename" @@ -34,6 +39,3 @@ public var Resource.Properties.Builder.mediaType: MediaType? put(FILENAME_KEY, value.toString()) } } - -public operator fun MediaTypeHints.Companion.invoke(properties: Resource.Properties): MediaTypeHints = - MediaTypeHints(mediaType = properties.mediaType) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt index cb008e373a..e074fb33d7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt @@ -12,7 +12,7 @@ import org.jsoup.Jsoup import org.jsoup.parser.Parser import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.readAsString import org.readium.r2.shared.util.mediatype.MediaType @@ -62,9 +62,9 @@ public class HtmlResourceContentExtractor : ResourceContentExtractor { .readAsString() .tryRecover { when (it) { - is DecoderError.Read -> + is DecodeError.Reading -> return@withContext Try.failure(it.cause) - is DecoderError.Decoding -> + is DecodeError.Decoding -> Try.success("") } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index 5fcb5e8fe7..c083188ab1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -37,13 +37,13 @@ internal class FileZipArchiveProvider { Try.failure(MediaTypeSnifferError.NotRecognized) } catch (e: SecurityException) { Try.failure( - MediaTypeSnifferError.Read( + MediaTypeSnifferError.Reading( ReadError.Access(FileSystemError.Forbidden(e)) ) ) } catch (e: IOException) { Try.failure( - MediaTypeSnifferError.Read( + MediaTypeSnifferError.Reading( ReadError.Access(FileSystemError.IO(e)) ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 2c6154e708..edccb7a54b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -39,7 +39,7 @@ internal class StreamingZipArchiveProvider { } catch (exception: Exception) { when (val e = exception.unwrapInstance(ReadException::class.java)) { is ReadException -> - Try.failure(MediaTypeSnifferError.Read(e.error)) + Try.failure(MediaTypeSnifferError.Reading(e.error)) else -> Try.failure(MediaTypeSnifferError.NotRecognized) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index 4029a02aab..ae6e3c2970 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -13,7 +13,7 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.data.CompositeContainer -import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.readAsRwpm import org.readium.r2.shared.util.getOrElse @@ -80,8 +80,8 @@ internal class ParserAssetFactory( val manifest = asset.resource.readAsRwpm() .mapFailure { when (it) { - is DecoderError.Decoding -> ReadError.Decoding(it.cause) - is DecoderError.Read -> it.cause + is DecodeError.Decoding -> ReadError.Decoding(it.cause) + is DecodeError.Reading -> it.cause } } .getOrElse { return Try.failure(Error.ReadError(it)) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 2ec7324c89..67ae455a38 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -223,7 +223,7 @@ public class PublicationFactory( ?.open(asset, credentials, allowUserInteraction) ?.mapFailure { when (it) { - is ContentProtection.Error.ReadError -> + is ContentProtection.Error.Reading -> Error.ReadError(it.cause) is ContentProtection.Error.UnsupportedAsset -> Error.UnsupportedAsset(it) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index 954eb2329e..958fb8e9cf 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -71,7 +71,7 @@ public class AudioParser( when (error) { MediaTypeSnifferError.NotRecognized -> null - is MediaTypeSnifferError.Read -> + is MediaTypeSnifferError.Reading -> return Try.failure(PublicationParser.Error.ReadError(error.cause)) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index b0075fb128..5d872b8d01 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -18,7 +18,7 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.readAsXml import org.readium.r2.shared.util.getOrElse @@ -192,14 +192,14 @@ public class EpubParser( private suspend fun Resource.decodeOrFail( url: Url, - decode: suspend Resource.() -> Try + decode: suspend Resource.() -> Try ): Try { return decode() .mapFailure { when (it) { - is DecoderError.Read -> + is DecodeError.Reading -> PublicationParser.Error.ReadError(it.cause) - is DecoderError.Decoding -> + is DecodeError.Decoding -> PublicationParser.Error.ReadError( ReadError.Decoding( MessageError( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index 0b2fd4af42..5e90056e15 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -70,7 +70,7 @@ public class ImageParser( when (error) { MediaTypeSnifferError.NotRecognized -> null - is MediaTypeSnifferError.Read -> + is MediaTypeSnifferError.Reading -> return Try.failure(PublicationParser.Error.ReadError(error.cause)) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index 6609aa4e9b..ec9dfb6c24 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -16,7 +16,7 @@ import org.readium.r2.shared.publication.services.positionsServiceFactory import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.DecoderError +import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.readAsRwpm import org.readium.r2.shared.util.getOrElse @@ -55,14 +55,14 @@ public class ReadiumWebPubParser( .readAsRwpm() .getOrElse { when (it) { - is DecoderError.Read -> + is DecodeError.Reading -> return Try.failure( PublicationParser.Error.ReadError( ReadError.Decoding(it.cause) ) ) - is DecoderError.Decoding -> + is DecodeError.Decoding -> return Try.failure( PublicationParser.Error.ReadError( ReadError.Decoding( diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index 2dcf1cf8d1..637020ccbf 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -40,7 +40,7 @@ sealed class PublicationError( operator fun invoke(error: AssetRetriever.Error): PublicationError = when (error) { - is AssetRetriever.Error.ReadError -> + is AssetRetriever.Error.Reading -> ReadError(error.cause) is AssetRetriever.Error.FormatNotSupported -> UnsupportedArchiveFormat(error) From 7b677d87e2502b0d100455da50e089084ff7bb04 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 29 Nov 2023 14:27:42 +0100 Subject: [PATCH 36/86] Various fixes --- .../pdfium/navigator/PdfiumEngineProvider.kt | 8 ++++---- .../pspdfkit/navigator/PsPdfKitEngineProvider.kt | 8 ++++---- .../{SimplePresentation.kt => SimpleOverflow.kt} | 4 ++-- .../org/readium/r2/navigator/VisualNavigator.kt | 11 ++++++----- .../r2/navigator/epub/EpubNavigatorFragment.kt | 12 ++++++------ .../r2/navigator/epub/EpubNavigatorViewModel.kt | 4 ++-- .../r2/navigator/image/ImageNavigatorFragment.kt | 10 +++++----- .../r2/navigator/pager/R2EpubPageFragment.kt | 4 ++-- .../readium/r2/navigator/pdf/PdfEngineProvider.kt | 4 ++-- .../r2/navigator/pdf/PdfNavigatorFragment.kt | 6 +++--- .../navigator/util/DirectionalNavigationAdapter.kt | 14 +++++++------- .../util/mediatype/MediaTypeRetrieverTest.kt | 3 +-- .../r2/streamer/parser/image/ImageParserTest.kt | 3 +-- .../r2/testapp/reader/VisualReaderFragment.kt | 2 +- 14 files changed, 46 insertions(+), 47 deletions(-) rename readium/navigator/src/main/java/org/readium/r2/navigator/{SimplePresentation.kt => SimpleOverflow.kt} (89%) diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt index c37de3ef6d..b485f6f822 100644 --- a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt @@ -8,8 +8,8 @@ package org.readium.adapter.pdfium.navigator import android.graphics.PointF import com.github.barteksc.pdfviewer.PDFView -import org.readium.r2.navigator.OverflowNavigator -import org.readium.r2.navigator.SimplePresentation +import org.readium.r2.navigator.Overflowable +import org.readium.r2.navigator.SimpleOverflow import org.readium.r2.navigator.input.TapEvent import org.readium.r2.navigator.pdf.PdfDocumentFragmentInput import org.readium.r2.navigator.pdf.PdfEngineProvider @@ -68,8 +68,8 @@ public class PdfiumEngineProvider( return settingsPolicy.settings(preferences) } - override fun computePresentation(settings: PdfiumSettings): OverflowNavigator.Presentation = - SimplePresentation( + override fun computePresentation(settings: PdfiumSettings): Overflowable.Overflow = + SimpleOverflow( readingProgression = settings.readingProgression, scroll = true, axis = settings.scrollAxis diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt index 5c3eb1be1d..10c6e533bf 100644 --- a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt @@ -8,8 +8,8 @@ package org.readium.adapter.pspdfkit.navigator import android.graphics.PointF import com.pspdfkit.configuration.PdfConfiguration -import org.readium.r2.navigator.OverflowNavigator -import org.readium.r2.navigator.SimplePresentation +import org.readium.r2.navigator.Overflowable +import org.readium.r2.navigator.SimpleOverflow import org.readium.r2.navigator.input.TapEvent import org.readium.r2.navigator.pdf.PdfDocumentFragmentInput import org.readium.r2.navigator.pdf.PdfEngineProvider @@ -68,8 +68,8 @@ public class PsPdfKitEngineProvider( return settingsPolicy.settings(preferences) } - override fun computePresentation(settings: PsPdfKitSettings): OverflowNavigator.Presentation = - SimplePresentation( + override fun computePresentation(settings: PsPdfKitSettings): Overflowable.Overflow = + SimpleOverflow( readingProgression = settings.readingProgression, scroll = settings.scroll, axis = if (settings.scroll) settings.scrollAxis else Axis.HORIZONTAL diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/SimplePresentation.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/SimpleOverflow.kt similarity index 89% rename from readium/navigator/src/main/java/org/readium/r2/navigator/SimplePresentation.kt rename to readium/navigator/src/main/java/org/readium/r2/navigator/SimpleOverflow.kt index be4d8b7e69..4eb96aea93 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/SimplePresentation.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/SimpleOverflow.kt @@ -13,8 +13,8 @@ import org.readium.r2.shared.InternalReadiumApi @InternalReadiumApi @OptIn(ExperimentalReadiumApi::class) -public data class SimplePresentation( +public data class SimpleOverflow( override val readingProgression: ReadingProgression, override val scroll: Boolean, override val axis: Axis -) : OverflowNavigator.Presentation +) : Overflowable.Overflow diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/VisualNavigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/VisualNavigator.kt index ca76a56a9d..5f52e5ddb9 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/VisualNavigator.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/VisualNavigator.kt @@ -8,6 +8,7 @@ package org.readium.r2.navigator import android.graphics.PointF import android.view.View +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.readium.r2.navigator.input.InputListener import org.readium.r2.navigator.preferences.Axis @@ -52,10 +53,10 @@ public interface VisualNavigator : Navigator { * Current presentation rendered by the navigator. */ @Deprecated( - "Moved to DirectionalNavigator", + "Moved to OverflowableNavigator.overflow", level = DeprecationLevel.ERROR ) - public val presentation: StateFlow + public val presentation: StateFlow get() = MutableStateFlow(Any()) /** * Returns the [Locator] to the first content element that begins on the current screen. @@ -129,7 +130,7 @@ public interface VisualNavigator : Navigator { * The user typically navigates through the publication by scrolling or tapping the viewport edges. */ @ExperimentalReadiumApi -public interface OverflowNavigator : VisualNavigator { +public interface Overflowable : VisualNavigator { @ExperimentalReadiumApi public interface Listener : VisualNavigator.Listener @@ -138,10 +139,10 @@ public interface OverflowNavigator : VisualNavigator { * Current presentation rendered by the navigator. */ @ExperimentalReadiumApi - public override val presentation: StateFlow + public val overflow: StateFlow @ExperimentalReadiumApi - public interface Presentation { + public interface Overflow { /** * Horizontal direction of progression across resources. */ diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt index f8a3355db5..7ad962fee0 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt @@ -48,7 +48,7 @@ import org.readium.r2.navigator.DecorationId import org.readium.r2.navigator.ExperimentalDecorator import org.readium.r2.navigator.HyperlinkNavigator import org.readium.r2.navigator.NavigatorFragment -import org.readium.r2.navigator.OverflowNavigator +import org.readium.r2.navigator.Overflowable import org.readium.r2.navigator.R import org.readium.r2.navigator.R2BasicWebView import org.readium.r2.navigator.SelectableNavigator @@ -116,7 +116,7 @@ public class EpubNavigatorFragment internal constructor( private val defaults: EpubDefaults, configuration: Configuration ) : NavigatorFragment(publication), - OverflowNavigator, + Overflowable, SelectableNavigator, DecorableNavigator, HyperlinkNavigator, @@ -259,7 +259,7 @@ public class EpubNavigatorFragment internal constructor( public fun onPageLoaded() {} } - public interface Listener : OverflowNavigator.Listener, HyperlinkNavigator.Listener + public interface Listener : Overflowable.Listener, HyperlinkNavigator.Listener // Configurable @@ -476,7 +476,7 @@ public class EpubNavigatorFragment internal constructor( } adapter.listener = PagerAdapterListener() resourcePager.adapter = adapter - resourcePager.direction = presentation.value.readingProgression + resourcePager.direction = overflow.value.readingProgression resourcePager.layoutDirection = when (settings.value.readingProgression) { ReadingProgression.RTL -> LayoutDirection.RTL ReadingProgression.LTR -> LayoutDirection.LTR @@ -663,8 +663,8 @@ public class EpubNavigatorFragment internal constructor( override val publicationView: View get() = requireView() - override val presentation: StateFlow - get() = viewModel.presentation + override val overflow: StateFlow + get() = viewModel.overflow @Deprecated( "Use `presentation.value.readingProgression` instead", diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt index a8ad16e7b9..2d576d98fb 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt @@ -85,9 +85,9 @@ internal class EpubNavigatorViewModel( val settings: StateFlow = _settings.asStateFlow() - val presentation: StateFlow = _settings + val overflow: StateFlow = _settings .mapStateIn(viewModelScope) { settings -> - SimplePresentation( + SimpleOverflow( readingProgression = settings.readingProgression, scroll = settings.scroll, axis = if (settings.scroll && !settings.verticalText) { diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt index c3c46998e4..9a0758404f 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt @@ -21,8 +21,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.runBlocking import org.readium.r2.navigator.NavigatorFragment -import org.readium.r2.navigator.OverflowNavigator -import org.readium.r2.navigator.SimplePresentation +import org.readium.r2.navigator.Overflowable +import org.readium.r2.navigator.SimpleOverflow import org.readium.r2.navigator.VisualNavigator import org.readium.r2.navigator.databinding.ReadiumNavigatorViewpagerBinding import org.readium.r2.navigator.extensions.layoutDirectionIsRTL @@ -54,7 +54,7 @@ public class ImageNavigatorFragment private constructor( publication: Publication, private val initialLocator: Locator? = null, internal val listener: Listener? = null -) : NavigatorFragment(publication), OverflowNavigator { +) : NavigatorFragment(publication), Overflowable { public interface Listener : VisualNavigator.Listener @@ -241,9 +241,9 @@ public class ImageNavigatorFragment private constructor( publication.metadata.effectiveReadingProgression @ExperimentalReadiumApi - override val presentation: StateFlow = + override val overflow: StateFlow = MutableStateFlow( - SimplePresentation( + SimpleOverflow( readingProgression = when (publication.metadata.readingProgression) { PublicationReadingProgression.RTL -> ReadingProgression.RTL else -> ReadingProgression.LTR diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2EpubPageFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2EpubPageFragment.kt index c48af893f4..55f0ea03c2 100755 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2EpubPageFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2EpubPageFragment.kt @@ -364,7 +364,7 @@ internal class R2EpubPageFragment : Fragment() { ?.let { locator -> loadLocator( webView, - requireNotNull(navigator).presentation.value.readingProgression, + requireNotNull(navigator).overflow.value.readingProgression, locator ) } @@ -385,7 +385,7 @@ internal class R2EpubPageFragment : Fragment() { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { val webView = requireNotNull(webView) val epubNavigator = requireNotNull(navigator) - loadLocator(webView, epubNavigator.presentation.value.readingProgression, locator) + loadLocator(webView, epubNavigator.overflow.value.readingProgression, locator) webView.listener?.onProgressionChanged() } } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt index 5995d3f90f..97d44fbc12 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt @@ -9,7 +9,7 @@ package org.readium.r2.navigator.pdf import androidx.fragment.app.Fragment import kotlinx.coroutines.flow.StateFlow import org.readium.r2.navigator.Navigator -import org.readium.r2.navigator.OverflowNavigator +import org.readium.r2.navigator.Overflowable import org.readium.r2.navigator.VisualNavigator import org.readium.r2.navigator.input.InputListener import org.readium.r2.navigator.preferences.Configurable @@ -41,7 +41,7 @@ public interface PdfEngineProvider -) : NavigatorFragment(publication), VisualNavigator, OverflowNavigator, Configurable { +) : NavigatorFragment(publication), VisualNavigator, Overflowable, Configurable { public interface Listener : VisualNavigator.Listener @@ -219,7 +219,7 @@ public class PdfNavigatorFragment + override val overflow: StateFlow get() = settings.mapStateIn(lifecycleScope) { settings -> pdfEngineProvider.computePresentation(settings) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/util/DirectionalNavigationAdapter.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/util/DirectionalNavigationAdapter.kt index 5d34f8cea8..3f6010b763 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/util/DirectionalNavigationAdapter.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/util/DirectionalNavigationAdapter.kt @@ -6,7 +6,7 @@ package org.readium.r2.navigator.util -import org.readium.r2.navigator.OverflowNavigator +import org.readium.r2.navigator.Overflowable import org.readium.r2.navigator.input.InputListener import org.readium.r2.navigator.input.Key import org.readium.r2.navigator.input.KeyEvent @@ -38,7 +38,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi */ @ExperimentalReadiumApi public class DirectionalNavigationAdapter( - private val navigator: OverflowNavigator, + private val navigator: Overflowable, private val tapEdges: Set = setOf(TapEdge.Horizontal), private val handleTapsWhileScrolling: Boolean = false, private val minimumHorizontalEdgeSize: Double = 80.0, @@ -56,7 +56,7 @@ public class DirectionalNavigationAdapter( } override fun onTap(event: TapEvent): Boolean { - if (navigator.presentation.value.scroll && !handleTapsWhileScrolling) { + if (navigator.overflow.value.scroll && !handleTapsWhileScrolling) { return false } @@ -112,8 +112,8 @@ public class DirectionalNavigationAdapter( /** * Moves to the left content portion (eg. page) relative to the reading progression direction. */ - private fun OverflowNavigator.goLeft(animated: Boolean = false): Boolean { - return when (presentation.value.readingProgression) { + private fun Overflowable.goLeft(animated: Boolean = false): Boolean { + return when (overflow.value.readingProgression) { ReadingProgression.LTR -> goBackward(animated = animated) @@ -125,8 +125,8 @@ public class DirectionalNavigationAdapter( /** * Moves to the right content portion (eg. page) relative to the reading progression direction. */ - private fun OverflowNavigator.goRight(animated: Boolean = false): Boolean { - return when (presentation.value.readingProgression) { + private fun Overflowable.goRight(animated: Boolean = false): Boolean { + return when (overflow.value.readingProgression) { ReadingProgression.LTR -> goForward(animated = animated) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt index daa29c865b..a263007d91 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt @@ -23,8 +23,7 @@ class MediaTypeRetrieverTest { private val retriever = MediaTypeRetriever( DefaultMediaTypeSniffer(), FormatRegistry(), - ZipArchiveFactory(), - null + ZipArchiveFactory() ) @Test diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index 205e711d4e..0664e7428d 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -39,8 +39,7 @@ class ImageParserTest { MediaTypeRetriever( DefaultMediaTypeSniffer(), FormatRegistry(), - ZipArchiveFactory(), - null + ZipArchiveFactory() ) private val parser = ImageParser(mediaTypeRetriever) diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt index b8ae8c7364..1511c7dd57 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt @@ -94,7 +94,7 @@ abstract class VisualReaderFragment : BaseReaderFragment() { navigatorFragment = navigator as Fragment - (navigator as OverflowNavigator).apply { + (navigator as Overflowable).apply { // This will automatically turn pages when tapping the screen edges or arrow keys. addInputListener(DirectionalNavigationAdapter(this)) } From 4b6b77fcfae770333029069787f4ec88c68e72af Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 29 Nov 2023 15:01:21 +0100 Subject: [PATCH 37/86] Renaming --- .../adapter/pdfium/document/PdfiumDocument.kt | 8 ++++---- .../adapter/pspdfkit/document/PsPdfKitDocument.kt | 4 ++-- .../pspdfkit/navigator/PsPdfKitDocumentFragment.kt | 8 ++++---- .../org/readium/r2/navigator/epub/HtmlInjector.kt | 3 +-- .../org/readium/r2/shared/publication/Publication.kt | 8 +++----- .../protection/LcpFallbackContentProtection.kt | 9 +++++---- .../publication/services/content/ContentService.kt | 7 ++++--- .../content/iterators/PublicationContentIterator.kt | 4 ++-- .../services/search/StringSearchService.kt | 6 ++++-- .../org/readium/r2/shared/util/pdf/PdfDocument.kt | 6 +++--- .../org/readium/r2/shared/util/resource/Resource.kt | 9 +++------ .../shared/util/zip/StreamingZipArchiveProvider.kt | 3 +-- .../r2/shared/util/zip/StreamingZipContainer.kt | 8 ++++---- .../org/readium/r2/streamer/PublicationFactory.kt | 2 +- .../readium/r2/streamer/parser/PublicationParser.kt | 7 ++++--- .../readium/r2/streamer/parser/audio/AudioParser.kt | 4 ++-- .../r2/streamer/parser/epub/EpubDeobfuscator.kt | 6 +++--- .../readium/r2/streamer/parser/epub/EpubParser.kt | 12 ++++++------ .../readium/r2/streamer/parser/image/ImageParser.kt | 4 ++-- .../org/readium/r2/streamer/parser/pdf/PdfParser.kt | 4 ++-- .../streamer/parser/readium/ReadiumWebPubParser.kt | 8 ++++---- .../streamer/parser/epub/EpubPositionsServiceTest.kt | 6 +++--- 22 files changed, 67 insertions(+), 69 deletions(-) diff --git a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt index 05914dbca3..7d074c051f 100644 --- a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt +++ b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt @@ -25,8 +25,8 @@ import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory +import org.readium.r2.shared.util.resource.ReadTry import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.use import timber.log.Timber @@ -89,14 +89,14 @@ public class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory { + override suspend fun open(resource: Resource, password: String?): ReadTry { // First try to open the resource as a file on the FS for performance improvement, as // PDFium requires the whole PDF document to be loaded in memory when using raw bytes. return resource.openAsFile(password) ?: resource.openBytes(password) } - private suspend fun Resource.openAsFile(password: String?): ResourceTry? = + private suspend fun Resource.openAsFile(password: String?): ReadTry? = tryOrNull { source?.toFile()?.let { file -> withContext(Dispatchers.IO) { @@ -105,7 +105,7 @@ public class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory = + private suspend fun Resource.openBytes(password: String?): ReadTry = use { it.read() .flatMap { bytes -> diff --git a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt index 844942415c..a20d2bb694 100644 --- a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt @@ -26,8 +26,8 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory +import org.readium.r2.shared.util.resource.ReadTry import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceTry import timber.log.Timber public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory { @@ -35,7 +35,7 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory = PsPdfKitDocument::class - override suspend fun open(resource: Resource, password: String?): ResourceTry = + override suspend fun open(resource: Resource, password: String?): ReadTry = withContext(Dispatchers.IO) { val dataProvider = ResourceDataProvider(resource) val documentSource = DocumentSource(dataProvider) diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt index 96740413f7..518956d304 100644 --- a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt @@ -56,7 +56,7 @@ import org.readium.r2.shared.publication.services.isProtected import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.pdf.cachedIn -import org.readium.r2.shared.util.resource.ResourceTry +import org.readium.r2.shared.util.resource.ReadTry import timber.log.Timber @ExperimentalReadiumApi @@ -90,13 +90,13 @@ public class PsPdfKitDocumentFragment internal constructor( private val psPdfKitListener = PsPdfKitListener() private class DocumentViewModel( - document: suspend () -> ResourceTry + document: suspend () -> ReadTry ) : ViewModel() { - private val _document: Deferred> = + private val _document: Deferred> = viewModelScope.async { document() } - suspend fun loadDocument(): ResourceTry = + suspend fun loadDocument(): ReadTry = _document.await() @OptIn(ExperimentalCoroutinesApi::class) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt index 7d4a8a1262..28ad1f3f31 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt @@ -17,7 +17,6 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.TransformingResource import timber.log.Timber @@ -36,7 +35,7 @@ internal fun Resource.injectHtml( ): Resource = TransformingResource(this) { bytes -> if (!mediaType.isHtml) { - return@TransformingResource ResourceTry.success(bytes) + return@TransformingResource Try.success(bytes) } var content = bytes.toString(mediaType.charset ?: Charsets.UTF_8).trim() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index 57fd269799..b89288c722 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -48,8 +48,6 @@ internal typealias ServiceFactory = (Publication.Service.Context) -> Publication */ public typealias PublicationId = String -public typealias PublicationContainer = Container - /** * The Publication shared model is the entry-point for all the metadata and services * related to a Readium publication. @@ -62,7 +60,7 @@ public typealias PublicationContainer = Container */ public class Publication( public val manifest: Manifest, - private val container: PublicationContainer = EmptyContainer(), + private val container: Container = EmptyContainer(), private val servicesBuilder: ServicesBuilder = ServicesBuilder(), httpClient: HttpClient? = null, @Deprecated( @@ -343,7 +341,7 @@ public class Publication( */ public class Context( public val manifest: Manifest, - public val container: PublicationContainer, + public val container: Container, public val services: PublicationServicesHolder ) @@ -459,7 +457,7 @@ public class Publication( */ public class Builder( public var manifest: Manifest, - public var container: PublicationContainer, + public var container: Container, public var servicesBuilder: ServicesBuilder = ServicesBuilder() ) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index 5d35000170..c1222f0e73 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -14,13 +14,14 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.readAsRwpm import org.readium.r2.shared.util.data.readAsXml import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ResourceContainer +import org.readium.r2.shared.util.resource.Resource /** * [ContentProtection] implementation used as a fallback by the Streamer to detect LCP DRM @@ -69,7 +70,7 @@ public class LcpFallbackContentProtection : ContentProtection { return Try.success(protectedFile) } - private suspend fun isLcpProtected(container: ResourceContainer, mediaType: MediaType): Try { + private suspend fun isLcpProtected(container: Container, mediaType: MediaType): Try { val isRpf = mediaType.isRpf val isEpub = mediaType.matches(MediaType.EPUB) @@ -90,7 +91,7 @@ public class LcpFallbackContentProtection : ContentProtection { } } - private suspend fun hasLcpSchemeInManifest(container: ResourceContainer): Try { + private suspend fun hasLcpSchemeInManifest(container: Container): Try { val manifest = container[Url("manifest.json")!!] ?.readAsRwpm() ?.getOrElse { @@ -110,7 +111,7 @@ public class LcpFallbackContentProtection : ContentProtection { return Try.success(manifestHasLcpScheme) } - private suspend fun hasLcpSchemeInEncryptionXml(container: ResourceContainer): Try { + private suspend fun hasLcpSchemeInEncryptionXml(container: Container): Try { val encryptionXml = container .get(Url("META-INF/encryption.xml")!!) ?.readAsXml() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentService.kt index aa017f1556..d21f160040 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/ContentService.kt @@ -8,10 +8,11 @@ package org.readium.r2.shared.publication.services.content import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.* -import org.readium.r2.shared.publication.PublicationContainer import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.publication.services.content.iterators.PublicationContentIterator import org.readium.r2.shared.publication.services.content.iterators.ResourceContentIteratorFactory +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.resource.Resource /** * Provides a way to extract the raw [Content] of a [Publication]. @@ -52,7 +53,7 @@ public var Publication.ServicesBuilder.contentServiceFactory: ServiceFactory? @ExperimentalReadiumApi public class DefaultContentService( private val manifest: Manifest, - private val container: PublicationContainer, + private val container: Container, private val services: PublicationServicesHolder, private val resourceContentIteratorFactories: List ) : ContentService { @@ -76,7 +77,7 @@ public class DefaultContentService( private inner class ContentImpl( val manifest: Manifest, - val container: PublicationContainer, + val container: Container, val services: PublicationServicesHolder, val start: Locator? ) : Content { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt index b7f77a29cd..6048bf4cdf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/PublicationContentIterator.kt @@ -10,11 +10,11 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Manifest -import org.readium.r2.shared.publication.PublicationContainer import org.readium.r2.shared.publication.PublicationServicesHolder import org.readium.r2.shared.publication.indexOfFirstWithHref import org.readium.r2.shared.publication.services.content.Content import org.readium.r2.shared.util.Either +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource @@ -55,7 +55,7 @@ public fun interface ResourceContentIteratorFactory { @ExperimentalReadiumApi public class PublicationContentIterator( private val manifest: Manifest, - private val container: PublicationContainer, + private val container: Container, private val services: PublicationServicesHolder, private val startLocator: Locator?, private val resourceContentIteratorFactories: List diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt index 0bf82e644b..be84a77258 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt @@ -23,7 +23,9 @@ import org.readium.r2.shared.publication.services.search.SearchService.Options 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.data.Container import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.content.DefaultResourceContentExtractorFactory import org.readium.r2.shared.util.resource.content.ResourceContentExtractor import timber.log.Timber @@ -41,7 +43,7 @@ import timber.log.Timber @ExperimentalReadiumApi public class StringSearchService( private val manifest: Manifest, - private val container: PublicationContainer, + private val container: Container, private val services: PublicationServicesHolder, private val language: String?, private val snippetLength: Int, @@ -91,7 +93,7 @@ public class StringSearchService( private inner class Iterator( val manifest: Manifest, - val container: PublicationContainer, + val container: Container, val query: String, val options: Options, val locale: Locale diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt index 152116521a..8a033c7d8c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt @@ -23,8 +23,8 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.cache.Cache import org.readium.r2.shared.util.cache.getOrTryPut import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.ReadTry import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceTry public interface PdfDocumentFactory { @@ -32,7 +32,7 @@ public interface PdfDocumentFactory { public val documentType: KClass /** Opens a PDF from a [resource]. */ - public suspend fun open(resource: Resource, password: String?): ResourceTry + public suspend fun open(resource: Resource, password: String?): ReadTry } /** @@ -56,7 +56,7 @@ private class CachingPdfDocumentFactory( private val cache: Cache ) : PdfDocumentFactory by factory { - override suspend fun open(resource: Resource, password: String?): ResourceTry { + override suspend fun open(resource: Resource, password: String?): ReadTry { val key = resource.source?.toString() ?: return factory.open(resource, password) return cache.transaction { getOrTryPut(key) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index b243d130bb..6de23627bd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -8,13 +8,10 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.Readable -public typealias ResourceTry = Try - -public typealias ResourceContainer = Container +public typealias ReadTry = Try /** * Acts as a proxy to an actual resource by handling read access. @@ -71,9 +68,9 @@ public class FailureResource( replaceWith = ReplaceWith("map(transform)") ) @Suppress("UnusedReceiverParameter") -public fun Try.mapCatching(): ResourceTry = +public fun Try.mapCatching(): ReadTry = throw NotImplementedError() @Suppress("UnusedReceiverParameter") -public fun Try.flatMapCatching(): ResourceTry = +public fun Try.flatMapCatching(): ReadTry = throw NotImplementedError() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index edccb7a54b..383c3bcb8f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -21,7 +21,6 @@ import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceContainer import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel @@ -84,7 +83,7 @@ internal class StreamingZipArchiveProvider { StreamingZipContainer(zipFile, sourceUrl) } - internal suspend fun openFile(file: File): ResourceContainer = withContext(Dispatchers.IO) { + internal suspend fun openFile(file: File): Container = withContext(Dispatchers.IO) { val fileChannel = FileChannelAdapter(file, "r") val channel = wrapBaseChannel(fileChannel) StreamingZipContainer(ZipFile(channel), file.toUrl()) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt index 78459a5b0e..536f054ce5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt @@ -24,8 +24,8 @@ import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.ReadTry import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.filename import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipArchiveEntry import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile @@ -42,7 +42,7 @@ internal class StreamingZipContainer( override val source: AbsoluteUrl? get() = null - override suspend fun properties(): ResourceTry = + override suspend fun properties(): ReadTry = Try.success( Resource.Properties { filename = url.filename @@ -54,7 +54,7 @@ internal class StreamingZipContainer( } ) - override suspend fun length(): ResourceTry = + override suspend fun length(): ReadTry = entry.size.takeUnless { it == -1L } ?.let { Try.success(it) } ?: Try.failure( @@ -71,7 +71,7 @@ internal class StreamingZipContainer( entry.compressedSize.takeUnless { it == -1L } } - override suspend fun read(range: LongRange?): ResourceTry = + override suspend fun read(range: LongRange?): ReadTry = withContext(Dispatchers.IO) { try { val bytes = diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 4f2ed4ad05..9b10509d17 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -276,7 +276,7 @@ public class PublicationFactory( when (e) { is PublicationParser.Error.UnsupportedFormat -> Error.UnsupportedAsset(MessageError("Cannot find a parser for this asset.")) - is PublicationParser.Error.ReadError -> + is PublicationParser.Error.Reading -> Error.ReadError(e.cause) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt index c2b1929b4f..186414cc60 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt @@ -8,10 +8,11 @@ package org.readium.r2.streamer.parser import kotlin.String import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.PublicationContainer import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource /** * Parses a Publication from an asset. @@ -28,7 +29,7 @@ public interface PublicationParser { */ public data class Asset( val mediaType: MediaType, - val container: PublicationContainer + val container: Container ) /** @@ -52,7 +53,7 @@ public interface PublicationParser { public class UnsupportedFormat : Error("Asset format not supported.", null) - public class ReadError(override val cause: org.readium.r2.shared.util.data.ReadError) : + public class Reading(override val cause: org.readium.r2.shared.util.data.ReadError) : Error("An error occurred while trying to read asset.", cause) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index 958fb8e9cf..d850db0d71 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -56,7 +56,7 @@ public class AudioParser( if (readingOrder.isEmpty()) { return Try.failure( - PublicationParser.Error.ReadError( + PublicationParser.Error.Reading( ReadError.Decoding( MessageError("No audio file found in the publication.") ) @@ -72,7 +72,7 @@ public class AudioParser( MediaTypeSnifferError.NotRecognized -> null is MediaTypeSnifferError.Reading -> - return Try.failure(PublicationParser.Error.ReadError(error.cause)) + return Try.failure(PublicationParser.Error.Reading(error.cause)) } } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt index 64c82679c2..66d930a347 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt @@ -11,8 +11,8 @@ import com.mcxiaoke.koi.ext.toHexBytes import kotlin.experimental.xor import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.resource.ReadTry import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceTry import org.readium.r2.shared.util.resource.TransformingResource import org.readium.r2.shared.util.resource.flatMap @@ -41,10 +41,10 @@ internal class EpubDeobfuscator( ) : TransformingResource(resource) { // The obfuscation doesn't change the length of the resource. - override suspend fun length(): ResourceTry = + override suspend fun length(): ReadTry = resource.length() - override suspend fun transform(data: ResourceTry): ResourceTry = + override suspend fun transform(data: ReadTry): ReadTry = data.map { bytes -> val obfuscationLength: Int = algorithm2length[algorithm] ?: return@map bytes diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 5d872b8d01..dc7dedc42f 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -54,7 +54,7 @@ public class EpubParser( .getOrElse { return Try.failure(it) } val opfResource = asset.container.get(opfPath) ?: return Try.failure( - PublicationParser.Error.ReadError( + PublicationParser.Error.Reading( ReadError.Decoding( MessageError("Missing OPF file.") ) @@ -65,7 +65,7 @@ public class EpubParser( .getOrElse { return Try.failure(it) } val packageDocument = PackageDocument.parse(opfXmlDocument, opfPath) ?: return Try.failure( - PublicationParser.Error.ReadError( + PublicationParser.Error.Reading( ReadError.Decoding( MessageError("Invalid OPF file.") ) @@ -112,7 +112,7 @@ public class EpubParser( val containerXmlResource = container .get(containerXmlUrl) ?: return Try.failure( - PublicationParser.Error.ReadError( + PublicationParser.Error.Reading( ReadError.Decoding("container.xml not found.") ) ) @@ -126,7 +126,7 @@ public class EpubParser( ?.let { Url.fromEpubHref(it) } ?.let { Try.success(it) } ?: Try.failure( - PublicationParser.Error.ReadError( + PublicationParser.Error.Reading( ReadError.Decoding("Cannot successfully parse OPF.") ) ) @@ -198,9 +198,9 @@ public class EpubParser( .mapFailure { when (it) { is DecodeError.Reading -> - PublicationParser.Error.ReadError(it.cause) + PublicationParser.Error.Reading(it.cause) is DecodeError.Decoding -> - PublicationParser.Error.ReadError( + PublicationParser.Error.Reading( ReadError.Decoding( MessageError( "Couldn't decode resource at $url", diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index 5e90056e15..de03187a87 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -55,7 +55,7 @@ public class ImageParser( if (readingOrder.isEmpty()) { return Try.failure( - PublicationParser.Error.ReadError( + PublicationParser.Error.Reading( ReadError.Decoding( MessageError("No bitmap found in the publication.") ) @@ -71,7 +71,7 @@ public class ImageParser( MediaTypeSnifferError.NotRecognized -> null is MediaTypeSnifferError.Reading -> - return Try.failure(PublicationParser.Error.ReadError(error.cause)) + return Try.failure(PublicationParser.Error.Reading(error.cause)) } } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index b81283fce8..128838b9d0 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -48,14 +48,14 @@ public class PdfParser( val resource = url ?.let { asset.container[it] } ?: return Try.failure( - PublicationParser.Error.ReadError( + PublicationParser.Error.Reading( ReadError.Decoding( MessageError("No PDF found in the publication.") ) ) ) val document = pdfFactory.open(resource, password = null) - .getOrElse { return Try.failure(PublicationParser.Error.ReadError(it)) } + .getOrElse { return Try.failure(PublicationParser.Error.Reading(it)) } val tableOfContents = document.outline.toLinks(url) val manifest = Manifest( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index ec9dfb6c24..efce8ba82a 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -44,7 +44,7 @@ public class ReadiumWebPubParser( val manifestResource = asset.container[Url("manifest.json")!!] ?: return Try.failure( - PublicationParser.Error.ReadError( + PublicationParser.Error.Reading( ReadError.Decoding( MessageError("Missing manifest.") ) @@ -57,14 +57,14 @@ public class ReadiumWebPubParser( when (it) { is DecodeError.Reading -> return Try.failure( - PublicationParser.Error.ReadError( + PublicationParser.Error.Reading( ReadError.Decoding(it.cause) ) ) is DecodeError.Decoding -> return Try.failure( - PublicationParser.Error.ReadError( + PublicationParser.Error.Reading( ReadError.Decoding( MessageError("Failed to parse the RWPM Manifest.") ) @@ -80,7 +80,7 @@ public class ReadiumWebPubParser( (readingOrder.isEmpty() || !readingOrder.all { MediaType.PDF.matches(it.mediaType) }) ) { return Try.failure( - PublicationParser.Error.ReadError( + PublicationParser.Error.Reading( ReadError.Decoding("Invalid LCP Protected PDF.") ) ) diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt index 099c325bcc..d9823e196c 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt @@ -25,8 +25,8 @@ import org.readium.r2.shared.util.archive.ArchiveProperties import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.ReadTry import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceTry import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -504,12 +504,12 @@ class EpubPositionsServiceTest { override val source: AbsoluteUrl? = null - override suspend fun properties(): ResourceTry = + override suspend fun properties(): ReadTry = Try.success(item.resourceProperties) override suspend fun length() = Try.success(item.length) - override suspend fun read(range: LongRange?): ResourceTry = + override suspend fun read(range: LongRange?): ReadTry = Try.success(ByteArray(0)) override suspend fun close() {} From 881772e871166b8f3aeeccabd80ba72694d8d6c1 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 29 Nov 2023 15:17:14 +0100 Subject: [PATCH 38/86] Naming --- .../readium/r2/lcp/LcpContentProtection.kt | 6 ++-- .../AdeptFallbackContentProtection.kt | 2 +- .../protection/ContentProtection.kt | 2 +- .../ContentProtectionSchemeRetriever.kt | 8 +++--- .../LcpFallbackContentProtection.kt | 2 +- .../readium/r2/streamer/PublicationFactory.kt | 28 +++++++++---------- .../r2/streamer/parser/PublicationParser.kt | 2 +- .../r2/streamer/parser/audio/AudioParser.kt | 2 +- .../r2/streamer/parser/epub/EpubParser.kt | 2 +- .../r2/streamer/parser/image/ImageParser.kt | 2 +- .../r2/streamer/parser/pdf/PdfParser.kt | 2 +- .../parser/readium/ReadiumWebPubParser.kt | 2 +- .../readium/r2/testapp/domain/Bookshelf.kt | 4 +-- .../r2/testapp/domain/PublicationError.kt | 10 +++---- 14 files changed, 37 insertions(+), 37 deletions(-) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index 15185c24ff..dabc578ad2 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -157,7 +157,7 @@ internal class LcpContentProtection( Try.success((it)) } else { Try.failure( - ContentProtection.Error.UnsupportedAsset( + ContentProtection.Error.AssetNotSupported( MessageError( "LCP license points to an unsupported publication." ) @@ -173,10 +173,10 @@ internal class LcpContentProtection( private fun AssetRetriever.Error.wrap(): ContentProtection.Error = when (this) { is AssetRetriever.Error.FormatNotSupported -> - ContentProtection.Error.UnsupportedAsset(this) + ContentProtection.Error.AssetNotSupported(this) is AssetRetriever.Error.Reading -> ContentProtection.Error.Reading(cause) is AssetRetriever.Error.SchemeNotSupported -> - ContentProtection.Error.UnsupportedAsset(this) + ContentProtection.Error.AssetNotSupported(this) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt index ccb796edc6..10a41f3556 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt @@ -43,7 +43,7 @@ public class AdeptFallbackContentProtection : ContentProtection { ): Try { if (asset !is Asset.Container) { return Try.failure( - ContentProtection.Error.UnsupportedAsset( + ContentProtection.Error.AssetNotSupported( MessageError("A container asset was expected.") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index 218e4043eb..4f259de723 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -41,7 +41,7 @@ public interface ContentProtection { override val cause: ReadError ) : Error("An error occurred while trying to read asset.", cause) - public class UnsupportedAsset( + public class AssetNotSupported( override val cause: org.readium.r2.shared.util.Error? ) : Error("Asset is not supported.", cause) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt index cdc26688f5..07c54529f5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt @@ -29,21 +29,21 @@ public class ContentProtectionSchemeRetriever( override val cause: org.readium.r2.shared.util.Error? ) : org.readium.r2.shared.util.Error { - public object NoContentProtectionFound : + public object NotRecognized : Error("No content protection recognized the given asset.", null) - public class ReadError(override val cause: org.readium.r2.shared.util.data.ReadError) : + public class Reading(override val cause: org.readium.r2.shared.util.data.ReadError) : Error("An error occurred while trying to read asset.", cause) } public suspend fun retrieve(asset: org.readium.r2.shared.util.asset.Asset): Try { for (protection in contentProtections) { protection.supports(asset) - .getOrElse { return Try.failure(Error.ReadError(it)) } + .getOrElse { return Try.failure(Error.Reading(it)) } .takeIf { it } ?.let { return Try.success(protection.scheme) } } - return Try.failure(Error.NoContentProtectionFound) + return Try.failure(Error.NotRecognized) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index c1222f0e73..e3a3cab4f4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -52,7 +52,7 @@ public class LcpFallbackContentProtection : ContentProtection { ): Try { if (asset !is Asset.Container) { return Try.failure( - ContentProtection.Error.UnsupportedAsset( + ContentProtection.Error.AssetNotSupported( MessageError("A container asset was expected.") ) ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 9b10509d17..3e08a517a3 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -65,15 +65,15 @@ public class PublicationFactory( override val cause: org.readium.r2.shared.util.Error? ) : org.readium.r2.shared.util.Error { - public class ReadError( + public class Reading( override val cause: org.readium.r2.shared.util.data.ReadError ) : Error("An error occurred while trying to read asset.", cause) - public class UnsupportedAsset( + public class FormatNotSupported( override val cause: org.readium.r2.shared.util.Error? ) : Error("Asset is not supported.", cause) - public class UnsupportedContentProtection( + public class ContentProtectionNotSupported( override val cause: org.readium.r2.shared.util.Error? = null ) : Error("No ContentProtection available to open asset.", cause) } @@ -199,9 +199,9 @@ public class PublicationFactory( .mapFailure { when (it) { is ParserAssetFactory.Error.ReadError -> - Error.ReadError(it.cause) + Error.Reading(it.cause) is ParserAssetFactory.Error.UnsupportedAsset -> - Error.UnsupportedAsset(it.cause) + Error.FormatNotSupported(it.cause) } } .getOrElse { return Try.failure(it) } @@ -221,13 +221,13 @@ public class PublicationFactory( ?.mapFailure { when (it) { is ContentProtection.Error.Reading -> - Error.ReadError(it.cause) - is ContentProtection.Error.UnsupportedAsset -> - Error.UnsupportedAsset(it) + Error.Reading(it.cause) + is ContentProtection.Error.AssetNotSupported -> + Error.FormatNotSupported(it) } } ?.getOrElse { return Try.failure(it) } - ?: return Try.failure(Error.UnsupportedContentProtection()) + ?: return Try.failure(Error.ContentProtectionNotSupported()) val parserAsset = PublicationParser.Asset( protectedAsset.mediaType, @@ -264,19 +264,19 @@ public class PublicationFactory( val result = parser.parse(publicationAsset, warnings) if ( result is Try.Success || - result is Try.Failure && result.value !is PublicationParser.Error.UnsupportedFormat + result is Try.Failure && result.value !is PublicationParser.Error.FormatNotSupported ) { return result } } - return Try.failure(PublicationParser.Error.UnsupportedFormat()) + return Try.failure(PublicationParser.Error.FormatNotSupported()) } private fun wrapParserException(e: PublicationParser.Error): Error = when (e) { - is PublicationParser.Error.UnsupportedFormat -> - Error.UnsupportedAsset(MessageError("Cannot find a parser for this asset.")) + is PublicationParser.Error.FormatNotSupported -> + Error.FormatNotSupported(MessageError("Cannot find a parser for this asset.")) is PublicationParser.Error.Reading -> - Error.ReadError(e.cause) + Error.Reading(e.cause) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt index 186414cc60..8a49f8d1ad 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt @@ -50,7 +50,7 @@ public interface PublicationParser { public override val cause: org.readium.r2.shared.util.Error? ) : org.readium.r2.shared.util.Error { - public class UnsupportedFormat : + public class FormatNotSupported : Error("Asset format not supported.", null) public class Reading(override val cause: org.readium.r2.shared.util.data.ReadError) : diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index d850db0d71..7d0b2c841c 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -40,7 +40,7 @@ public class AudioParser( warnings: WarningLogger? ): Try { if (!asset.mediaType.matches(MediaType.ZAB) && !asset.mediaType.isAudio) { - return Try.failure(PublicationParser.Error.UnsupportedFormat()) + return Try.failure(PublicationParser.Error.FormatNotSupported()) } val readingOrder = diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index dc7dedc42f..bf16e31d41 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -47,7 +47,7 @@ public class EpubParser( warnings: WarningLogger? ): Try { if (asset.mediaType != MediaType.EPUB) { - return Try.failure(PublicationParser.Error.UnsupportedFormat()) + return Try.failure(PublicationParser.Error.FormatNotSupported()) } val opfPath = getRootFilePath(asset.container) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index de03187a87..e54475de8b 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -41,7 +41,7 @@ public class ImageParser( warnings: WarningLogger? ): Try { if (!asset.mediaType.matches(MediaType.CBZ) && !asset.mediaType.isBitmap) { - return Try.failure(PublicationParser.Error.UnsupportedFormat()) + return Try.failure(PublicationParser.Error.FormatNotSupported()) } val readingOrder = diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index 128838b9d0..5332c30399 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -39,7 +39,7 @@ public class PdfParser( warnings: WarningLogger? ): Try { if (asset.mediaType != MediaType.PDF) { - return Try.failure(PublicationParser.Error.UnsupportedFormat()) + return Try.failure(PublicationParser.Error.FormatNotSupported()) } val url = asset.container.entries diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index efce8ba82a..ed8317c1a5 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -39,7 +39,7 @@ public class ReadiumWebPubParser( warnings: WarningLogger? ): Try { if (!asset.mediaType.isReadiumWebPublication) { - return Try.failure(PublicationParser.Error.UnsupportedFormat()) + return Try.failure(PublicationParser.Error.FormatNotSupported()) } val manifestResource = asset.container[Url("manifest.json")!!] diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index fb2c1531ce..9d2becd377 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -137,9 +137,9 @@ class Bookshelf( protectionRetriever.retrieve(asset) .tryRecover { when (it) { - ContentProtectionSchemeRetriever.Error.NoContentProtectionFound -> + ContentProtectionSchemeRetriever.Error.NotRecognized -> Try.success(null) - is ContentProtectionSchemeRetriever.Error.ReadError -> + is ContentProtectionSchemeRetriever.Error.Reading -> Try.failure(it) } }.getOrElse { diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index 637020ccbf..1555e98842 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -50,19 +50,19 @@ sealed class PublicationError( operator fun invoke(error: ContentProtectionSchemeRetriever.Error): PublicationError = when (error) { - is ContentProtectionSchemeRetriever.Error.ReadError -> + is ContentProtectionSchemeRetriever.Error.Reading -> ReadError(error.cause) - ContentProtectionSchemeRetriever.Error.NoContentProtectionFound -> + ContentProtectionSchemeRetriever.Error.NotRecognized -> UnsupportedContentProtection(error) } operator fun invoke(error: PublicationFactory.Error): PublicationError = when (error) { - is PublicationFactory.Error.ReadError -> + is PublicationFactory.Error.Reading -> ReadError(error.cause) - is PublicationFactory.Error.UnsupportedAsset -> + is PublicationFactory.Error.FormatNotSupported -> UnsupportedPublication(error) - is PublicationFactory.Error.UnsupportedContentProtection -> + is PublicationFactory.Error.ContentProtectionNotSupported -> UnsupportedContentProtection(error) } } From 41ebe8fd93f86c7a6cfd528c795406ffe4ea3fd7 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 29 Nov 2023 16:10:01 +0100 Subject: [PATCH 39/86] Moves --- .../org/readium/r2/shared/util/asset/AssetRetriever.kt | 1 + .../readium/r2/shared/util/asset/MediaTypeRetriever.kt | 2 +- .../shared/util/{data => content}/ContentResolverError.kt | 3 ++- .../shared/util/{resource => content}/ContentResource.kt | 6 ++++-- .../util/{asset => content}/ContentResourceFactory.kt | 4 ++-- .../r2/shared/util/{resource => file}/FileResource.kt | 6 ++++-- .../r2/shared/util/{asset => file}/FileResourceFactory.kt | 4 ++-- .../r2/shared/util/{data => file}/FileSystemError.kt | 3 ++- .../r2/shared/util/{asset => http}/HttpResourceFactory.kt | 5 ++--- .../readium/r2/shared/util/resource/DirectoryContainer.kt | 3 ++- .../r2/shared/util/{asset => resource}/ResourceFactory.kt | 3 +-- .../readium/r2/shared/util/zip/FileZipArchiveProvider.kt | 2 +- .../org/readium/r2/shared/util/zip/FileZipContainer.kt | 2 +- .../r2/shared/publication/services/CoverServiceTest.kt | 2 +- .../r2/shared/util/resource/BufferingResourceTest.kt | 1 + .../r2/shared/util/resource/ResourceInputStreamTest.kt | 1 + .../readium/r2/shared/util/resource/ZipContainerTest.kt | 1 + .../readium/r2/streamer/parser/image/ImageParserTest.kt | 2 +- test-app/src/main/java/org/readium/r2/testapp/Readium.kt | 8 ++++---- .../main/java/org/readium/r2/testapp/domain/Bookshelf.kt | 2 +- .../org/readium/r2/testapp/domain/PublicationRetriever.kt | 2 +- .../java/org/readium/r2/testapp/domain/ReadUserError.kt | 4 ++-- 22 files changed, 38 insertions(+), 29 deletions(-) rename readium/shared/src/main/java/org/readium/r2/shared/util/{data => content}/ContentResolverError.kt (92%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource => content}/ContentResource.kt (96%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{asset => content}/ContentResourceFactory.kt (90%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource => file}/FileResource.kt (95%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{asset => file}/FileResourceFactory.kt (89%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{data => file}/FileSystemError.kt (92%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{asset => http}/HttpResourceFactory.kt (85%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{asset => resource}/ResourceFactory.kt (95%) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 5f2f22d565..01b0c78841 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -17,6 +17,7 @@ import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceFactory import org.readium.r2.shared.util.toUrl /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt index 6514d2a5ec..3b9207394a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt @@ -12,13 +12,13 @@ import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.archive.SmartArchiveFactory import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.getOrElse 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.MediaTypeSniffer import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.resource.FileResource import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentResolverError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt similarity index 92% rename from readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentResolverError.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt index b81789dd09..153cec8e8a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ContentResolverError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt @@ -4,10 +4,11 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.data +package org.readium.r2.shared.util.content import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.data.AccessError /** * Errors wrapping Android Content Provider errors. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt similarity index 96% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt index d496473efc..75550479ba 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.content import android.content.ContentResolver import android.net.Uri @@ -19,10 +19,12 @@ import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.ContentResolverError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.resource.mediaType import org.readium.r2.shared.util.toUrl /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt similarity index 90% rename from readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt index c7f9bbc70d..e6a41c3732 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ContentResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt @@ -4,14 +4,14 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.asset +package org.readium.r2.shared.util.content import android.content.ContentResolver import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ContentResource import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceFactory import org.readium.r2.shared.util.toUri /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt similarity index 95% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt index dd3a01596a..3c7ee14c2f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.file import java.io.File import java.io.FileNotFoundException @@ -17,11 +17,13 @@ import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.isLazyInitialized import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.resource.mediaType import org.readium.r2.shared.util.toUrl /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt similarity index 89% rename from readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt index 239d1c9898..3732c1780a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/FileResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt @@ -4,13 +4,13 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.asset +package org.readium.r2.shared.util.file import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.FileResource import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceFactory /** * Creates [FileResource]s. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileSystemError.kt similarity index 92% rename from readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/file/FileSystemError.kt index e0150c6466..1f94583104 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/FileSystemError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileSystemError.kt @@ -4,10 +4,11 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.data +package org.readium.r2.shared.util.file import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.data.AccessError /** * Errors wrapping file system exceptions. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt similarity index 85% rename from readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt index 9653e5f805..f5e556fee5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/HttpResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt @@ -4,14 +4,13 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.asset +package org.readium.r2.shared.util.http import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpResource import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceFactory /** * Creates [HttpResource]s. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt index 70de63c953..7a1b2eedef 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt @@ -13,7 +13,8 @@ import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.FileSystemError +import org.readium.r2.shared.util.file.FileResource +import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.toUrl /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt similarity index 95% rename from readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt index c67835e1ff..8d90a79c48 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.asset +package org.readium.r2.shared.util.resource import kotlin.String import kotlin.let @@ -12,7 +12,6 @@ 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.mediatype.MediaType -import org.readium.r2.shared.util.resource.Resource /** * A factory to read [Resource]s from [Url]s. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index c083188ab1..c7fb3a9038 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -16,8 +16,8 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.archive.ArchiveFactory import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt index 5a97cf4ce2..a72dfa2c84 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt @@ -23,8 +23,8 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.archive.ArchiveProperties import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream import org.readium.r2.shared.util.mediatype.MediaType diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt index d3f7214c10..04f8cb2143 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/services/CoverServiceTest.kt @@ -20,8 +20,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.publication.* import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.FileResource import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.shared.util.toAbsoluteUrl import org.robolectric.RobolectricTestRunner diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt index 9fa63a2b3c..8a5386d63c 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt @@ -8,6 +8,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt index b22c505fdc..30503fc7c1 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt @@ -7,6 +7,7 @@ import kotlin.test.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.util.data.ReadableInputStream +import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt index b061642a10..c020a99a1e 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -21,6 +21,7 @@ import org.junit.runner.RunWith import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.use import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index 0664e7428d..9755ee6a1f 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -22,9 +22,9 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.asset.DefaultMediaTypeSniffer import org.readium.r2.shared.util.asset.MediaTypeRetriever +import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.FileResource import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.zip.ZipArchiveFactory diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 4232415a28..58c924b320 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -18,15 +18,15 @@ import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetri import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.asset.CompositeResourceFactory -import org.readium.r2.shared.util.asset.ContentResourceFactory import org.readium.r2.shared.util.asset.DefaultMediaTypeSniffer -import org.readium.r2.shared.util.asset.FileResourceFactory -import org.readium.r2.shared.util.asset.HttpResourceFactory import org.readium.r2.shared.util.asset.MediaTypeRetriever +import org.readium.r2.shared.util.content.ContentResourceFactory import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager +import org.readium.r2.shared.util.file.FileResourceFactory import org.readium.r2.shared.util.http.DefaultHttpClient +import org.readium.r2.shared.util.http.HttpResourceFactory import org.readium.r2.shared.util.mediatype.FormatRegistry +import org.readium.r2.shared.util.resource.CompositeResourceFactory import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.readium.r2.streamer.PublicationFactory diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index 9d2becd377..89274cba49 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -18,8 +18,8 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.tryRecover diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index 127a0ad27c..65d42b59d4 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -25,9 +25,9 @@ import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.data.FileSystemError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt index be750380b7..fa6ad9265a 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt @@ -8,9 +8,9 @@ package org.readium.r2.testapp.domain import androidx.annotation.StringRes import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.data.ContentResolverError -import org.readium.r2.shared.util.data.FileSystemError +import org.readium.r2.shared.util.content.ContentResolverError import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.testapp.R From faea60c697a43fb5014ae6b62152381ea970a40c Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 29 Nov 2023 17:05:19 +0100 Subject: [PATCH 40/86] Cosmetic changes --- .../org/readium/r2/shared/util/Deprecated.kt | 2 +- .../java/org/readium/r2/shared/util/Ref.kt | 18 -------------- .../java/org/readium/r2/shared/util/Try.kt | 24 +++++++++---------- .../{resource => file}/DirectoryContainer.kt | 5 ++-- .../r2/shared/util/zip/ZipMediaTypeSniffer.kt | 8 +++++-- .../util/resource/DirectoryContainerTest.kt | 1 + .../shared/util/resource/ZipContainerTest.kt | 1 + .../parser/epub/EpubDeobfuscatorTest.kt | 2 +- 8 files changed, 24 insertions(+), 37 deletions(-) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/Ref.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource => file}/DirectoryContainer.kt (91%) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Deprecated.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Deprecated.kt index 9019ce948b..ae9be4fd3a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Deprecated.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Deprecated.kt @@ -12,7 +12,7 @@ package org.readium.r2.shared.util * Returns the encapsulated result of the given transform function applied to the encapsulated |Throwable] exception * if this instance represents failure or the original encapsulated value if it is success. */ -@Suppress("Unused_parameter") +@Suppress("Unused_parameter", "UnusedReceiverParameter") @Deprecated( message = "Use getOrElse instead.", level = DeprecationLevel.ERROR, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Ref.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Ref.kt deleted file mode 100644 index 7fbb0c70ce..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Ref.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2021 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util - -/** - * Smart pointer holding a mutable reference to an object. - * - * Get the reference by calling `ref()` - * Conveniently, the reference can be reset by setting the `ref` property. - */ -internal class Ref(var ref: T? = null) { - - operator fun invoke(): T? = ref -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt index 19981caa4a..523e91d0ca 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt @@ -109,8 +109,8 @@ public sealed class Try { */ public fun Try.getOrThrow(): S = when (this) { - is Try.Success -> value - is Try.Failure -> throw value + is Success -> value + is Failure -> throw value } /** @@ -118,8 +118,8 @@ public fun Try.getOrThrow(): S = */ public fun Try.getOrDefault(defaultValue: R): R = when (this) { - is Try.Success -> value - is Try.Failure -> defaultValue + is Success -> value + is Failure -> defaultValue } /** @@ -128,8 +128,8 @@ public fun Try.getOrDefault(defaultValue: R): R = */ public inline fun Try.getOrElse(onFailure: (exception: F) -> R): R = when (this) { - is Try.Success -> value - is Try.Failure -> onFailure(value) + is Success -> value + is Failure -> onFailure(value) } /** @@ -138,8 +138,8 @@ public inline fun Try.getOrElse(onFailure: (exception: F) -> */ public inline fun Try.flatMap(transform: (value: S) -> Try): Try = when (this) { - is Try.Success -> transform(value) - is Try.Failure -> Try.failure(value) + is Success -> transform(value) + is Failure -> Try.failure(value) } /** @@ -148,15 +148,15 @@ public inline fun Try.flatMap(transform: (value: S) -> Try */ public inline fun Try.tryRecover(transform: (exception: F) -> Try): Try = when (this) { - is Try.Success -> Try.success(value) - is Try.Failure -> transform(value) + is Success -> Try.success(value) + is Failure -> transform(value) } public fun Try.assertSuccess(): S = when (this) { - is Try.Success -> + is Success -> value - is Try.Failure -> { + is Failure -> { throw IllegalStateException( "Try was excepted to contain a success.", value as? Throwable ?: (value as? Error)?.let { ErrorException(it) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/DirectoryContainer.kt similarity index 91% rename from readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/file/DirectoryContainer.kt index 7a1b2eedef..98b877386b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/DirectoryContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/DirectoryContainer.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.resource +package org.readium.r2.shared.util.file import java.io.File import kotlinx.coroutines.Dispatchers @@ -13,8 +13,7 @@ import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.file.FileResource -import org.readium.r2.shared.util.file.FileSystemError +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.toUrl /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt index 7c7aca5849..b823fb1611 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt @@ -16,6 +16,10 @@ import org.readium.r2.shared.util.resource.Resource public object ZipMediaTypeSniffer : MediaTypeSniffer { + private val fileZipArchiveProvider = FileZipArchiveProvider() + + private val streamingZipArchiveProvider = StreamingZipArchiveProvider() + override fun sniffHints(hints: MediaTypeHints): Try { if (hints.hasMediaType("application/zip") || hints.hasFileExtension("zip") @@ -28,8 +32,8 @@ public object ZipMediaTypeSniffer : MediaTypeSniffer { override suspend fun sniffBlob(readable: Readable): Try { (readable as? Resource)?.source?.toFile() - ?.let { return FileZipArchiveProvider().sniffFile(it) } + ?.let { return fileZipArchiveProvider.sniffFile(it) } - return StreamingZipArchiveProvider().sniffBlob(readable) + return streamingZipArchiveProvider.sniffBlob(readable) } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt index 0e184d2266..a359aff57c 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt @@ -23,6 +23,7 @@ import org.readium.r2.shared.readBlocking import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.file.DirectoryContainer import org.readium.r2.shared.util.toAbsoluteUrl import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt index c020a99a1e..33f60d9a87 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -21,6 +21,7 @@ import org.junit.runner.RunWith import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.assertSuccess import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.file.DirectoryContainer import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.use diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt index e5665d4bfe..561079c37a 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt @@ -18,7 +18,7 @@ import org.junit.runner.RunWith import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.assertSuccess -import org.readium.r2.shared.util.resource.DirectoryContainer +import org.readium.r2.shared.util.file.DirectoryContainer import org.readium.r2.shared.util.resource.Resource import org.readium.r2.streamer.readBlocking import org.robolectric.RobolectricTestRunner From c0d3f86a70e201549c3d112a4d83f7dc061f434b Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 29 Nov 2023 17:55:20 +0100 Subject: [PATCH 41/86] Cosmetic changes --- .../r2/shared/util/archive/ArchiveFactory.kt | 8 +++-- .../util/archive/SmartArchiveFactory.kt | 2 +- .../r2/shared/util/asset/AssetRetriever.kt | 6 ++-- .../shared/util/asset/MediaTypeRetriever.kt | 29 +++++++++++++++++-- .../asset/SimpleResourceMediaTypeRetriever.kt | 2 +- .../util/content/ContentResolverError.kt | 2 +- .../r2/shared/util/content/ContentResource.kt | 4 +++ .../util/content/ContentResourceFactory.kt | 2 +- .../readium/r2/shared/util/data/ReadError.kt | 4 +-- .../r2/shared/util/file/FileResource.kt | 3 ++ .../shared/util/file/FileResourceFactory.kt | 2 +- .../shared/util/mediatype/FormatRegistry.kt | 4 +-- .../r2/shared/util/resource/StringResource.kt | 13 +++------ .../shared/util/zip/FileZipArchiveProvider.kt | 8 ++--- .../util/zip/StreamingZipArchiveProvider.kt | 4 +-- .../r2/shared/util/zip/ZipMediaTypeSniffer.kt | 3 ++ 16 files changed, 64 insertions(+), 32 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt index 624e1b8519..cc2354b94a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt @@ -14,7 +14,7 @@ import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource /** - * A factory to create [Container]s from archive [Resource]. + * A factory to create [Container]s from archive [Resource]s. */ public interface ArchiveFactory { @@ -28,7 +28,7 @@ public interface ArchiveFactory { cause: org.readium.r2.shared.util.Error? = null ) : Error("Media type not supported.", cause) - public class ReadError( + public class Reading( override val cause: org.readium.r2.shared.util.data.ReadError ) : Error("An error occurred while attempting to read the resource.", cause) } @@ -42,6 +42,10 @@ public interface ArchiveFactory { ): Try, Error> } +/** + * A composite [ArchiveFactory] which tries several factories until it finds one which supports + * the format. +*/ public class CompositeArchiveFactory( private val factories: List ) : ArchiveFactory { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt index ce4cb555e0..51b0665a0a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt @@ -31,7 +31,7 @@ internal class SmartArchiveFactory( ?.let { create(it, readable) } ?: Try.failure(error) } - is ArchiveFactory.Error.ReadError -> + is ArchiveFactory.Error.Reading -> Try.failure(error) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 01b0c78841..ad6f397e19 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -64,7 +64,7 @@ public class AssetRetriever( val archive = archiveFactory.create(mediaType, resource) .getOrElse { return when (it) { - is ArchiveFactory.Error.ReadError -> + is ArchiveFactory.Error.Reading -> Try.failure(Error.Reading(it.cause)) is ArchiveFactory.Error.FormatNotSupported -> Try.success(Asset.Resource(mediaType, resource)) @@ -96,7 +96,7 @@ public class AssetRetriever( retrieve(file.toUrl()) /** - * Retrieves an asset from an unknown [Url]. + * Retrieves an asset from an unknown [AbsoluteUrl]. */ public suspend fun retrieve(url: AbsoluteUrl): Try { val resource = resourceFactory.create(url) @@ -121,7 +121,7 @@ public class AssetRetriever( val container = archiveFactory.create(mediaType, resource) .getOrElse { when (it) { - is ArchiveFactory.Error.ReadError -> + is ArchiveFactory.Error.Reading -> return Try.failure(Error.Reading(it.cause)) is ArchiveFactory.Error.FormatNotSupported -> return Try.success(Asset.Resource(mediaType, resource)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt index 3b9207394a..7b193859d7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt @@ -23,11 +23,11 @@ import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use /** - * Retrieves a canonical [MediaType] for the provided media type and file extension hints and/or + * Retrieves a canonical [MediaType] for the provided media type and file extension hints and * asset content. * * The actual format sniffing is mostly done by the provided [mediaTypeSniffer]. - * The [DefaultMediaTypeSniffer] cover the formats supported with Readium by default. + * The [DefaultMediaTypeSniffer] covers the formats supported with Readium by default. */ public class MediaTypeRetriever( private val mediaTypeSniffer: MediaTypeSniffer, @@ -43,6 +43,8 @@ public class MediaTypeRetriever( /** * Retrieves a canonical [MediaType] for the provided media type and file extension [hints]. + * + * Useful for testing purpose. */ internal fun retrieve(hints: MediaTypeHints): MediaType? = simpleResourceMediaTypeRetriever.retrieveUnsafe(hints) @@ -50,6 +52,8 @@ public class MediaTypeRetriever( /** * Retrieves a canonical [MediaType] for the provided [mediaType] and [fileExtension] hints. + * + * Useful for testing purpose. */ internal fun retrieve(mediaType: String? = null, fileExtension: String? = null): MediaType? = retrieve( @@ -61,6 +65,8 @@ public class MediaTypeRetriever( /** * Retrieves a canonical [MediaType] for the provided [mediaType] and [fileExtension] hints. + * + * Useful for testing purpose. */ internal fun retrieve(mediaType: MediaType, fileExtension: String? = null): MediaType = @@ -68,6 +74,8 @@ public class MediaTypeRetriever( /** * Retrieves a canonical [MediaType] for the provided [mediaTypes] and [fileExtensions] hints. + * + * Useful for testing purpose. */ internal fun retrieve( mediaTypes: List = emptyList(), @@ -75,6 +83,12 @@ public class MediaTypeRetriever( ): MediaType? = retrieve(MediaTypeHints(mediaTypes = mediaTypes, fileExtensions = fileExtensions)) + /** + * Retrieves a canonical [MediaType] for [container]. + * + * @param container the resource to retrieve the media type of + * @param hints media type hints + */ public suspend fun retrieve( container: Container, hints: MediaTypeHints = MediaTypeHints() @@ -94,6 +108,12 @@ public class MediaTypeRetriever( return simpleResourceMediaTypeRetriever.retrieveUnsafe(hints) } + /** + * Retrieves a canonical [MediaType] for [file]. + * + * @param file the file to retrieve the media type of + * @param hints additional hints which will be added to those provided by the resource + */ public suspend fun retrieve( file: File, hints: MediaTypeHints = MediaTypeHints() @@ -102,6 +122,9 @@ public class MediaTypeRetriever( /** * Retrieves a canonical [MediaType] for [resource]. + * + * @param resource the resource to retrieve the media type of + * @param hints additional hints which will be added to those provided by the resource */ public suspend fun retrieve( resource: Resource, @@ -113,7 +136,7 @@ public class MediaTypeRetriever( val container = archiveFactory.create(resourceMediaType, resource) .getOrElse { when (it) { - is ArchiveFactory.Error.ReadError -> + is ArchiveFactory.Error.Reading -> return Try.failure(MediaTypeSnifferError.Reading(it.cause)) is ArchiveFactory.Error.FormatNotSupported -> return Try.success(resourceMediaType) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt index aef36680b9..783b5e44ee 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt @@ -20,7 +20,7 @@ import org.readium.r2.shared.util.resource.mediaType import org.readium.r2.shared.util.tryRecover /** - * A [MediaTypeRetriever] which does not open archive resources. + * A simple [MediaTypeRetriever] which does not open archive resources. */ internal class SimpleResourceMediaTypeRetriever( private val mediaTypeSniffer: MediaTypeSniffer, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt index 153cec8e8a..c19cb95614 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt @@ -11,7 +11,7 @@ import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.data.AccessError /** - * Errors wrapping Android Content Provider errors. + * Errors wrapping Android ContentResolver errors. */ public sealed class ContentResolverError( override val message: String, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt index 75550479ba..499bf2db27 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt @@ -29,6 +29,10 @@ import org.readium.r2.shared.util.toUrl /** * A [Resource] to access content [uri] thanks to a [ContentResolver]. + * + * @param uri the [Uri] to read. + * @param contentResolver a ContentResolver. + * @param mediaType the file media type, if already known. */ public class ContentResource( private val uri: Uri, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt index e6a41c3732..7d55c57cac 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.util.resource.ResourceFactory import org.readium.r2.shared.util.toUri /** - * Creates [ContentResource]s. + * Creates [ContentResource]s from Urls. */ public class ContentResourceFactory( private val contentResolver: ContentResolver diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt index 42ef0256d8..32dcfa1917 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt @@ -46,8 +46,8 @@ public sealed class ReadError( /** * Marker interface for source-specific access errors. * - * At the moment, [AccessError]s constructed by the toolkit can be either a [FileSystemError], - * a [ContentResolverError] or an HttpError. + * At the moment, [AccessError]s constructed by the toolkit can be either a FileSystemError, + * a ContentResolverError or an HttpError. */ public interface AccessError : Error diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt index 3c7ee14c2f..9c3891830b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt @@ -28,6 +28,9 @@ import org.readium.r2.shared.util.toUrl /** * A [Resource] to access a [File]. + * + * @param file the file to read. + * @param mediaType the file media type, if already known. */ public class FileResource( private val file: File, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt index 3732c1780a..193a6da3fd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt @@ -13,7 +13,7 @@ import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceFactory /** - * Creates [FileResource]s. + * Creates [FileResource]s from Urls. */ public class FileResourceFactory : ResourceFactory { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt index 66685dc832..42af6a2093 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt @@ -57,7 +57,7 @@ public class FormatRegistry( private val superTypes: MutableMap = superTypes.toMutableMap() /** - * Registers a new [fileExtension] for the given [mediaType]. + * Registers format data for the given [mediaType]. */ public fun register( mediaType: MediaType, @@ -90,7 +90,7 @@ public class FormatRegistry( superTypes[mediaType] /** - * Returns if [mediaType] is a generic type that could be used instead of more specific + * Returns if [mediaType] is a generic type that could have been used instead of more specific * media types. */ public fun isSuperType(mediaType: MediaType): Boolean = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt index 51530c5eca..e610bfc27b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt @@ -10,19 +10,17 @@ import kotlinx.coroutines.runBlocking import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.Readable /** Creates a Resource serving a [String]. */ -public class StringResource( - private val readable: Readable, - private val properties: Resource.Properties -) : Resource, Readable by readable { +public class StringResource private constructor( + private val resource: Resource +) : Resource by resource { public constructor( source: AbsoluteUrl? = null, properties: Resource.Properties = Resource.Properties(), string: suspend () -> Try - ) : this(InMemoryResource(source, properties) { string().map { it.toByteArray() } }, properties) + ) : this(InMemoryResource(source, properties) { string().map { it.toByteArray() } }) public constructor( string: String, @@ -33,9 +31,6 @@ public class StringResource( override val source: AbsoluteUrl? = null - override suspend fun properties(): Try = - Try.success(properties) - override fun toString(): String = "${javaClass.simpleName}(${runBlocking { read().map { it.decodeToString() } } }})" } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index c7fb3a9038..493c30c764 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -75,25 +75,25 @@ internal class FileZipArchiveProvider { Try.success(archive) } catch (e: FileNotFoundException) { Try.failure( - ArchiveFactory.Error.ReadError( + ArchiveFactory.Error.Reading( ReadError.Access(FileSystemError.NotFound(e)) ) ) } catch (e: ZipException) { Try.failure( - ArchiveFactory.Error.ReadError( + ArchiveFactory.Error.Reading( ReadError.Decoding(e) ) ) } catch (e: SecurityException) { Try.failure( - ArchiveFactory.Error.ReadError( + ArchiveFactory.Error.Reading( ReadError.Access(FileSystemError.Forbidden(e)) ) ) } catch (e: IOException) { Try.failure( - ArchiveFactory.Error.ReadError( + ArchiveFactory.Error.Reading( ReadError.Access(FileSystemError.IO(e)) ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 383c3bcb8f..39b0f1886d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -65,9 +65,9 @@ internal class StreamingZipArchiveProvider { } catch (exception: Exception) { when (val e = exception.unwrapInstance(ReadException::class.java)) { is ReadException -> - Try.failure(ArchiveFactory.Error.ReadError(e.error)) + Try.failure(ArchiveFactory.Error.Reading(e.error)) else -> - Try.failure(ArchiveFactory.Error.ReadError(ReadError.Decoding(e))) + Try.failure(ArchiveFactory.Error.Reading(ReadError.Decoding(e))) } } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt index b823fb1611..0e4c9e7e26 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt @@ -14,6 +14,9 @@ import org.readium.r2.shared.util.mediatype.MediaTypeSniffer import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource +/** + * Sniffs a ZIP archive. + */ public object ZipMediaTypeSniffer : MediaTypeSniffer { private val fileZipArchiveProvider = FileZipArchiveProvider() From 8f4002f37b62271f569dd5a57fe7ca4039ef3d2b Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 29 Nov 2023 18:00:24 +0100 Subject: [PATCH 42/86] Small fixes --- .../java/org/readium/adapter/pdfium/document/PdfiumDocument.kt | 2 +- .../org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt | 2 +- .../adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt | 2 +- .../src/main/java/org/readium/r2/shared/util/data/ReadError.kt | 2 ++ .../main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt | 2 +- .../java/org/readium/r2/shared/util/resource/FileProperties.kt | 2 +- .../main/java/org/readium/r2/shared/util/resource/Resource.kt | 3 +-- .../org/readium/r2/shared/util/zip/StreamingZipContainer.kt | 2 +- .../org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt | 2 +- .../r2/streamer/parser/epub/EpubPositionsServiceTest.kt | 2 +- 10 files changed, 11 insertions(+), 10 deletions(-) diff --git a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt index 7d074c051f..999440461d 100644 --- a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt +++ b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt @@ -22,10 +22,10 @@ import org.readium.r2.shared.extensions.unwrapInstance import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory -import org.readium.r2.shared.util.resource.ReadTry import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use import timber.log.Timber diff --git a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt index a20d2bb694..6253e8cd4c 100644 --- a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt @@ -24,9 +24,9 @@ import org.readium.r2.shared.publication.ReadingProgression import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.pdf.PdfDocument import org.readium.r2.shared.util.pdf.PdfDocumentFactory -import org.readium.r2.shared.util.resource.ReadTry import org.readium.r2.shared.util.resource.Resource import timber.log.Timber diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt index 518956d304..a2e0532f94 100644 --- a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitDocumentFragment.kt @@ -55,8 +55,8 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.isProtected import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.pdf.cachedIn -import org.readium.r2.shared.util.resource.ReadTry import timber.log.Timber @ExperimentalReadiumApi diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt index 32dcfa1917..bffd677bf1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt @@ -11,6 +11,7 @@ import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.Try /** * Errors occurring while reading a resource. @@ -59,3 +60,4 @@ public interface AccessError : Error public class ReadException( public val error: ReadError ) : IOException(error.message, ErrorException(error)) +public typealias ReadTry = Try diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt index 8a033c7d8c..82de3bb5d9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt @@ -22,8 +22,8 @@ import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.cache.Cache import org.readium.r2.shared.util.cache.getOrTryPut +import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ReadTry import org.readium.r2.shared.util.resource.Resource public interface PdfDocumentFactory { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt index de758f5f87..6ec5837be4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FileProperties.kt @@ -36,6 +36,6 @@ public var Resource.Properties.Builder.mediaType: MediaType? if (value == null) { remove(MEDIA_TYPE_KEY) } else { - put(FILENAME_KEY, value.toString()) + put(MEDIA_TYPE_KEY, value.toString()) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index 6de23627bd..8019722ca1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -9,10 +9,9 @@ package org.readium.r2.shared.util.resource import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.data.Readable -public typealias ReadTry = Try - /** * Acts as a proxy to an actual resource by handling read access. */ diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt index 536f054ce5..4ed26c1812 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt @@ -21,10 +21,10 @@ import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException +import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ReadTry import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.filename import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipArchiveEntry diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt index 66d930a347..3f485fb4dc 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt @@ -11,7 +11,7 @@ import com.mcxiaoke.koi.ext.toHexBytes import kotlin.experimental.xor import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.ReadTry +import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.TransformingResource import org.readium.r2.shared.util.resource.flatMap diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt index d9823e196c..1eaafba7c0 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt @@ -24,8 +24,8 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.archive.ArchiveProperties import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.ReadTry import org.readium.r2.shared.util.resource.Resource import org.robolectric.RobolectricTestRunner From fd211adf7c5e5ab07b2eb0648903e4f6e717e2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Thu, 30 Nov 2023 13:47:36 +0100 Subject: [PATCH 43/86] Remove FLAG_SECURE --- .../readium/r2/navigator/NavigatorFragment.kt | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/NavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/NavigatorFragment.kt index 26cc990b90..cb945295b6 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/NavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/NavigatorFragment.kt @@ -7,10 +7,8 @@ package org.readium.r2.navigator import android.os.Bundle -import android.view.WindowManager import androidx.fragment.app.Fragment import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.services.isProtected import org.readium.r2.shared.publication.services.isRestricted public abstract class NavigatorFragment internal constructor( @@ -22,21 +20,4 @@ public abstract class NavigatorFragment internal constructor( super.onCreate(savedInstanceState) } - - override fun onResume() { - super.onResume() - - // Prevent screenshots and text selection from the task switcher for protected publications. - if (publication.isProtected) { - activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE) - } - } - - override fun onStop() { - super.onStop() - - if (publication.isProtected) { - activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } - } } From 1d7f8965f046cfc6820069efbb01cedfde787a5b Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Thu, 30 Nov 2023 17:57:10 +0100 Subject: [PATCH 44/86] Fix ReadableInputStream lifecycle issues --- .../r2/navigator/epub/WebViewServer.kt | 7 ++-- ...{ReadableInputStream.kt => InputStream.kt} | 36 ++++++++++++++++--- .../shared/util/mediatype/MediaTypeSniffer.kt | 6 ++-- .../r2/shared/util/resource/Resource.kt | 15 ++++++++ .../util/resource/SingleResourceContainer.kt | 11 +----- ...t.kt => ReadableInputStreamAdapterTest.kt} | 8 ++--- 6 files changed, 59 insertions(+), 24 deletions(-) rename readium/shared/src/main/java/org/readium/r2/shared/util/data/{ReadableInputStream.kt => InputStream.kt} (80%) rename readium/shared/src/test/java/org/readium/r2/shared/util/resource/{ResourceInputStreamTest.kt => ReadableInputStreamAdapterTest.kt} (79%) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt index 290803fae4..f4461f5468 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt @@ -22,8 +22,7 @@ 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.data.ReadError -import org.readium.r2.shared.util.data.ReadException -import org.readium.r2.shared.util.data.ReadableInputStream +import org.readium.r2.shared.util.data.asInputStream import org.readium.r2.shared.util.http.HttpHeaders import org.readium.r2.shared.util.http.HttpRange import org.readium.r2.shared.util.resource.Resource @@ -127,10 +126,10 @@ internal class WebViewServer( 200, "OK", headers, - ReadableInputStream(resource, ::ReadException) + resource.asInputStream() ) } else { // Byte range request - val stream = ReadableInputStream(resource, ::ReadException) + val stream = resource.asInputStream() val length = stream.available() val longRange = range.toLongRange(length.toLong()) headers["Content-Range"] = "bytes ${longRange.first}-${longRange.last}/$length" diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadableInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/InputStream.kt similarity index 80% rename from readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadableInputStream.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/InputStream.kt index a11710f2aa..c7e70cdebc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadableInputStream.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/InputStream.kt @@ -12,15 +12,26 @@ import kotlinx.coroutines.runBlocking import org.readium.r2.shared.util.Try /** - * Input stream reading through a [Readable]. + * Wraps a [Readable] into an [InputStream]. + * + * Ownership of the [Readable] is transferred to the returned [InputStream]. + */ +public fun Readable.asInputStream( + range: LongRange? = null, + wrapError: (ReadError) -> IOException = { ReadException(it) } +): InputStream = + ReadableInputStreamAdapter(this, range, wrapError) + +/** + * Input stream reading through a [Readable] and taking ownership of it. * * If you experience bad performances, consider wrapping the stream in a BufferedInputStream. This * is particularly useful when streaming deflated ZIP entries. */ -public class ReadableInputStream( +private class ReadableInputStreamAdapter( private val readable: Readable, - private val wrapError: (ReadError) -> IOException = { ReadException(it) }, - private val range: LongRange? = null + private val range: LongRange? = null, + private val wrapError: (ReadError) -> IOException = { ReadException(it) } ) : InputStream() { private var isClosed = false @@ -122,6 +133,8 @@ public class ReadableInputStream( return } + runBlocking { readable.close() } + isClosed = true } } @@ -142,3 +155,18 @@ public class ReadableInputStream( } } } + +/** + * Returns a new [Readable] accessing the same data but not owning them. + */ +public fun Readable.borrow(): Readable = + BorrowedReadable(this) + +private class BorrowedReadable( + private val readable: Readable +) : Readable by readable { + + override suspend fun close() { + // Do nothing + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index da5f2e94a7..37fff78219 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -26,7 +26,8 @@ import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.Readable -import org.readium.r2.shared.util.data.ReadableInputStream +import org.readium.r2.shared.util.data.asInputStream +import org.readium.r2.shared.util.data.borrow import org.readium.r2.shared.util.data.containsJsonKeys import org.readium.r2.shared.util.data.readAsJson import org.readium.r2.shared.util.data.readAsRwpm @@ -893,7 +894,8 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { } override suspend fun sniffBlob(readable: Readable): Try { - ReadableInputStream(readable, ::SystemSnifferException) + readable.borrow() + .asInputStream(wrapError = ::SystemSnifferException) .use { stream -> try { withContext(Dispatchers.IO) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index 8019722ca1..f8da39ef4e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -61,6 +61,21 @@ public class FailureResource( "${javaClass.simpleName}($error)" } +private class BorrowedResource( + private val resource: Resource +) : Resource by resource { + + override suspend fun close() { + // Do nothing + } +} + +/** + * Returns a new [Resource] accessing the same data but not owning them. + */ +public fun Resource.borrow(): Resource = + BorrowedResource(this) + @Deprecated( "Catch exceptions yourself to the most suitable ReadError.", level = DeprecationLevel.ERROR, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt index df27f9e565..084b87e68f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SingleResourceContainer.kt @@ -15,15 +15,6 @@ public class SingleResourceContainer( private val resource: Resource ) : Container { - private class Entry( - private val resource: Resource - ) : Resource by resource { - - override suspend fun close() { - // Do nothing - } - } - override val entries: Set = setOf(entryUrl) override fun get(url: Url): Resource? { @@ -31,7 +22,7 @@ public class SingleResourceContainer( return null } - return Entry(resource) + return resource.borrow() } override suspend fun close() { diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ReadableInputStreamAdapterTest.kt similarity index 79% rename from readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt rename to readium/shared/src/test/java/org/readium/r2/shared/util/resource/ReadableInputStreamAdapterTest.kt index 30503fc7c1..eb77d9a063 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ResourceInputStreamTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ReadableInputStreamAdapterTest.kt @@ -6,16 +6,16 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue import org.junit.Test import org.junit.runner.RunWith -import org.readium.r2.shared.util.data.ReadableInputStream +import org.readium.r2.shared.util.data.asInputStream import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class ResourceInputStreamTest { +class ReadableInputStreamAdapterTest { private val file = File( - assertNotNull(ResourceInputStreamTest::class.java.getResource("epub.epub")?.path) + assertNotNull(ReadableInputStreamAdapterTest::class.java.getResource("epub.epub")?.path) ) private val fileContent: ByteArray = file.readBytes() private val bufferSize = 16384 // This is the size used by NanoHTTPd for chunked responses @@ -23,7 +23,7 @@ class ResourceInputStreamTest { @Test fun `stream can be read by chunks`() { val resource = FileResource(file, mediaType = MediaType.EPUB) - val resourceStream = ReadableInputStream(resource) + val resourceStream = resource.asInputStream() val outputStream = ByteArrayOutputStream(fileContent.size) resourceStream.copyTo(outputStream, bufferSize = bufferSize) assertTrue(fileContent.contentEquals(outputStream.toByteArray())) From 57d51832c82c84264a29748ebd55c0ef52b6ee5b Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Thu, 30 Nov 2023 17:58:16 +0100 Subject: [PATCH 45/86] Rename OverflowableNavigator --- .../adapter/pdfium/navigator/PdfiumEngineProvider.kt | 4 ++-- .../adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt | 4 ++-- .../main/java/org/readium/r2/navigator/SimpleOverflow.kt | 2 +- .../main/java/org/readium/r2/navigator/VisualNavigator.kt | 2 +- .../readium/r2/navigator/epub/EpubNavigatorFragment.kt | 8 ++++---- .../readium/r2/navigator/epub/EpubNavigatorViewModel.kt | 2 +- .../readium/r2/navigator/image/ImageNavigatorFragment.kt | 6 +++--- .../org/readium/r2/navigator/pdf/PdfEngineProvider.kt | 4 ++-- .../org/readium/r2/navigator/pdf/PdfNavigatorFragment.kt | 6 +++--- .../r2/navigator/util/DirectionalNavigationAdapter.kt | 8 ++++---- .../org/readium/r2/testapp/reader/VisualReaderFragment.kt | 2 +- 11 files changed, 24 insertions(+), 24 deletions(-) diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt index b485f6f822..689c247e29 100644 --- a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt @@ -8,7 +8,7 @@ package org.readium.adapter.pdfium.navigator import android.graphics.PointF import com.github.barteksc.pdfviewer.PDFView -import org.readium.r2.navigator.Overflowable +import org.readium.r2.navigator.OverflowableNavigator import org.readium.r2.navigator.SimpleOverflow import org.readium.r2.navigator.input.TapEvent import org.readium.r2.navigator.pdf.PdfDocumentFragmentInput @@ -68,7 +68,7 @@ public class PdfiumEngineProvider( return settingsPolicy.settings(preferences) } - override fun computePresentation(settings: PdfiumSettings): Overflowable.Overflow = + override fun computePresentation(settings: PdfiumSettings): OverflowableNavigator.Overflow = SimpleOverflow( readingProgression = settings.readingProgression, scroll = true, diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt index 10c6e533bf..dc6899443e 100644 --- a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt @@ -8,7 +8,7 @@ package org.readium.adapter.pspdfkit.navigator import android.graphics.PointF import com.pspdfkit.configuration.PdfConfiguration -import org.readium.r2.navigator.Overflowable +import org.readium.r2.navigator.OverflowableNavigator import org.readium.r2.navigator.SimpleOverflow import org.readium.r2.navigator.input.TapEvent import org.readium.r2.navigator.pdf.PdfDocumentFragmentInput @@ -68,7 +68,7 @@ public class PsPdfKitEngineProvider( return settingsPolicy.settings(preferences) } - override fun computePresentation(settings: PsPdfKitSettings): Overflowable.Overflow = + override fun computePresentation(settings: PsPdfKitSettings): OverflowableNavigator.Overflow = SimpleOverflow( readingProgression = settings.readingProgression, scroll = settings.scroll, diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/SimpleOverflow.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/SimpleOverflow.kt index 4eb96aea93..afbe6f3604 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/SimpleOverflow.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/SimpleOverflow.kt @@ -17,4 +17,4 @@ public data class SimpleOverflow( override val readingProgression: ReadingProgression, override val scroll: Boolean, override val axis: Axis -) : Overflowable.Overflow +) : OverflowableNavigator.Overflow diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/VisualNavigator.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/VisualNavigator.kt index 5f52e5ddb9..543fabdb26 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/VisualNavigator.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/VisualNavigator.kt @@ -130,7 +130,7 @@ public interface VisualNavigator : Navigator { * The user typically navigates through the publication by scrolling or tapping the viewport edges. */ @ExperimentalReadiumApi -public interface Overflowable : VisualNavigator { +public interface OverflowableNavigator : VisualNavigator { @ExperimentalReadiumApi public interface Listener : VisualNavigator.Listener diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt index 7ad962fee0..44dc9cf301 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt @@ -48,7 +48,7 @@ import org.readium.r2.navigator.DecorationId import org.readium.r2.navigator.ExperimentalDecorator import org.readium.r2.navigator.HyperlinkNavigator import org.readium.r2.navigator.NavigatorFragment -import org.readium.r2.navigator.Overflowable +import org.readium.r2.navigator.OverflowableNavigator import org.readium.r2.navigator.R import org.readium.r2.navigator.R2BasicWebView import org.readium.r2.navigator.SelectableNavigator @@ -116,7 +116,7 @@ public class EpubNavigatorFragment internal constructor( private val defaults: EpubDefaults, configuration: Configuration ) : NavigatorFragment(publication), - Overflowable, + OverflowableNavigator, SelectableNavigator, DecorableNavigator, HyperlinkNavigator, @@ -259,7 +259,7 @@ public class EpubNavigatorFragment internal constructor( public fun onPageLoaded() {} } - public interface Listener : Overflowable.Listener, HyperlinkNavigator.Listener + public interface Listener : OverflowableNavigator.Listener, HyperlinkNavigator.Listener // Configurable @@ -663,7 +663,7 @@ public class EpubNavigatorFragment internal constructor( override val publicationView: View get() = requireView() - override val overflow: StateFlow + override val overflow: StateFlow get() = viewModel.overflow @Deprecated( diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt index 2d576d98fb..6b66d5ee0b 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorViewModel.kt @@ -85,7 +85,7 @@ internal class EpubNavigatorViewModel( val settings: StateFlow = _settings.asStateFlow() - val overflow: StateFlow = _settings + val overflow: StateFlow = _settings .mapStateIn(viewModelScope) { settings -> SimpleOverflow( readingProgression = settings.readingProgression, diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt index 9a0758404f..39365f99af 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/image/ImageNavigatorFragment.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.runBlocking import org.readium.r2.navigator.NavigatorFragment -import org.readium.r2.navigator.Overflowable +import org.readium.r2.navigator.OverflowableNavigator import org.readium.r2.navigator.SimpleOverflow import org.readium.r2.navigator.VisualNavigator import org.readium.r2.navigator.databinding.ReadiumNavigatorViewpagerBinding @@ -54,7 +54,7 @@ public class ImageNavigatorFragment private constructor( publication: Publication, private val initialLocator: Locator? = null, internal val listener: Listener? = null -) : NavigatorFragment(publication), Overflowable { +) : NavigatorFragment(publication), OverflowableNavigator { public interface Listener : VisualNavigator.Listener @@ -241,7 +241,7 @@ public class ImageNavigatorFragment private constructor( publication.metadata.effectiveReadingProgression @ExperimentalReadiumApi - override val overflow: StateFlow = + override val overflow: StateFlow = MutableStateFlow( SimpleOverflow( readingProgression = when (publication.metadata.readingProgression) { diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt index 97d44fbc12..bd715ee150 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt @@ -9,7 +9,7 @@ package org.readium.r2.navigator.pdf import androidx.fragment.app.Fragment import kotlinx.coroutines.flow.StateFlow import org.readium.r2.navigator.Navigator -import org.readium.r2.navigator.Overflowable +import org.readium.r2.navigator.OverflowableNavigator import org.readium.r2.navigator.VisualNavigator import org.readium.r2.navigator.input.InputListener import org.readium.r2.navigator.preferences.Configurable @@ -41,7 +41,7 @@ public interface PdfEngineProvider -) : NavigatorFragment(publication), VisualNavigator, Overflowable, Configurable { +) : NavigatorFragment(publication), VisualNavigator, OverflowableNavigator, Configurable { public interface Listener : VisualNavigator.Listener @@ -219,7 +219,7 @@ public class PdfNavigatorFragment + override val overflow: StateFlow get() = settings.mapStateIn(lifecycleScope) { settings -> pdfEngineProvider.computePresentation(settings) } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/util/DirectionalNavigationAdapter.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/util/DirectionalNavigationAdapter.kt index 3f6010b763..1089cc7b4b 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/util/DirectionalNavigationAdapter.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/util/DirectionalNavigationAdapter.kt @@ -6,7 +6,7 @@ package org.readium.r2.navigator.util -import org.readium.r2.navigator.Overflowable +import org.readium.r2.navigator.OverflowableNavigator import org.readium.r2.navigator.input.InputListener import org.readium.r2.navigator.input.Key import org.readium.r2.navigator.input.KeyEvent @@ -38,7 +38,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi */ @ExperimentalReadiumApi public class DirectionalNavigationAdapter( - private val navigator: Overflowable, + private val navigator: OverflowableNavigator, private val tapEdges: Set = setOf(TapEdge.Horizontal), private val handleTapsWhileScrolling: Boolean = false, private val minimumHorizontalEdgeSize: Double = 80.0, @@ -112,7 +112,7 @@ public class DirectionalNavigationAdapter( /** * Moves to the left content portion (eg. page) relative to the reading progression direction. */ - private fun Overflowable.goLeft(animated: Boolean = false): Boolean { + private fun OverflowableNavigator.goLeft(animated: Boolean = false): Boolean { return when (overflow.value.readingProgression) { ReadingProgression.LTR -> goBackward(animated = animated) @@ -125,7 +125,7 @@ public class DirectionalNavigationAdapter( /** * Moves to the right content portion (eg. page) relative to the reading progression direction. */ - private fun Overflowable.goRight(animated: Boolean = false): Boolean { + private fun OverflowableNavigator.goRight(animated: Boolean = false): Boolean { return when (overflow.value.readingProgression) { ReadingProgression.LTR -> goForward(animated = animated) diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt index 1511c7dd57..0439791ba1 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt @@ -94,7 +94,7 @@ abstract class VisualReaderFragment : BaseReaderFragment() { navigatorFragment = navigator as Fragment - (navigator as Overflowable).apply { + (navigator as OverflowableNavigator).apply { // This will automatically turn pages when tapping the screen edges or arrow keys. addInputListener(DirectionalNavigationAdapter(this)) } From 6755380c2790ed8a25d65b0641723c6afe3cf2d5 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Thu, 30 Nov 2023 18:04:41 +0100 Subject: [PATCH 46/86] Cosmetic changes --- .../util/asset/DefaultMediaTypeSniffer.kt | 4 +- .../shared/util/mediatype/MediaTypeSniffer.kt | 86 ++++++++++--------- .../r2/shared/util/zip/ZipMediaTypeSniffer.kt | 6 +- 3 files changed, 50 insertions(+), 46 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/DefaultMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/DefaultMediaTypeSniffer.kt index a45875387c..47940e8e11 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/DefaultMediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/DefaultMediaTypeSniffer.kt @@ -62,8 +62,8 @@ public class DefaultMediaTypeSniffer : MediaTypeSniffer { override fun sniffHints(hints: MediaTypeHints): Try = sniffer.sniffHints(hints) - override suspend fun sniffBlob(readable: Readable): Try = - sniffer.sniffBlob(readable) + override suspend fun sniffBlob(source: Readable): Try = + sniffer.sniffBlob(source) override suspend fun sniffContainer(container: Container<*>): Try = sniffer.sniffContainer(container) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index 37fff78219..cbda2349cd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -46,19 +46,32 @@ public sealed class MediaTypeSnifferError( public data class Reading(override val cause: ReadError) : MediaTypeSnifferError("An error occurred while trying to read content.", cause) } + +/** + * Sniffs a [MediaType] from media type and file extension hints. + */ public interface HintMediaTypeSniffer { + public fun sniffHints( hints: MediaTypeHints ): Try } +/** + * Sniffs a [MediaType] from a [Readable] blob. + */ public interface BlobMediaTypeSniffer { + public suspend fun sniffBlob( - readable: Readable + source: Readable ): Try } +/** + * Sniffs a [MediaType] from a [Container]. + */ public interface ContainerMediaTypeSniffer { + public suspend fun sniffContainer( container: Container ): Try @@ -72,25 +85,16 @@ public interface MediaTypeSniffer : BlobMediaTypeSniffer, ContainerMediaTypeSniffer { - /** - * Sniffs a [MediaType] from media type and file extension hints. - */ public override fun sniffHints( hints: MediaTypeHints ): Try = Try.failure(MediaTypeSnifferError.NotRecognized) - /** - * Sniffs a [MediaType] from a [Readable]. - */ public override suspend fun sniffBlob( - readable: Readable + source: Readable ): Try = Try.failure(MediaTypeSnifferError.NotRecognized) - /** - * Sniffs a [MediaType] from a [Container]. - */ public override suspend fun sniffContainer( container: Container ): Try = @@ -113,9 +117,9 @@ public class CompositeMediaTypeSniffer( return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(readable: Readable): Try { + override suspend fun sniffBlob(source: Readable): Try { for (sniffer in sniffers) { - sniffer.sniffBlob(readable) + sniffer.sniffBlob(source) .getOrElse { error -> when (error) { MediaTypeSnifferError.NotRecognized -> @@ -165,12 +169,12 @@ public object XhtmlMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(readable: Readable): Try { - if (!readable.canReadWholeBlob()) { + override suspend fun sniffBlob(source: Readable): Try { + if (!source.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } - readable.readAsXml() + source.readAsXml() .getOrElse { when (it) { is DecodeError.Reading -> @@ -205,13 +209,13 @@ public object HtmlMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(readable: Readable): Try { - if (!readable.canReadWholeBlob()) { + override suspend fun sniffBlob(source: Readable): Try { + if (!source.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } // [contentAsXml] will fail if the HTML is not a proper XML document, hence the doctype check. - readable.readAsXml() + source.readAsXml() .getOrElse { when (it) { is DecodeError.Reading -> @@ -223,7 +227,7 @@ public object HtmlMediaTypeSniffer : MediaTypeSniffer { ?.takeIf { it.name.lowercase(Locale.ROOT) == "html" } ?.let { return Try.success(MediaType.HTML) } - readable.readAsString() + source.readAsString() .getOrElse { when (it) { is DecodeError.Reading -> @@ -277,13 +281,13 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(readable: Readable): Try { - if (!readable.canReadWholeBlob()) { + override suspend fun sniffBlob(source: Readable): Try { + if (!source.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } // OPDS 1 - readable.readAsXml() + source.readAsXml() .getOrElse { when (it) { is DecodeError.Reading -> @@ -302,7 +306,7 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { } // OPDS 2 - readable.readAsRwpm() + source.readAsRwpm() .getOrElse { when (it) { is DecodeError.Reading -> @@ -334,7 +338,7 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { } // OPDS Authentication Document. - readable.containsJsonKeys("id", "title", "authentication") + source.containsJsonKeys("id", "title", "authentication") .getOrElse { when (it) { is DecodeError.Reading -> @@ -364,12 +368,12 @@ public object LcpLicenseMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(readable: Readable): Try { - if (!readable.canReadWholeBlob()) { + override suspend fun sniffBlob(source: Readable): Try { + if (!source.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } - readable.containsJsonKeys("id", "issued", "provider", "encryption") + source.containsJsonKeys("id", "issued", "provider", "encryption") .getOrElse { when (it) { is DecodeError.Reading -> @@ -459,13 +463,13 @@ public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - public override suspend fun sniffBlob(readable: Readable): Try { - if (!readable.canReadWholeBlob()) { + public override suspend fun sniffBlob(source: Readable): Try { + if (!source.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } val manifest: Manifest = - readable.readAsRwpm() + source.readAsRwpm() .getOrElse { when (it) { is DecodeError.Reading -> @@ -568,13 +572,13 @@ public object WebPubMediaTypeSniffer : MediaTypeSniffer { /** Sniffs a W3C Web Publication Manifest. */ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { - override suspend fun sniffBlob(readable: Readable): Try { - if (!readable.canReadWholeBlob()) { + override suspend fun sniffBlob(source: Readable): Try { + if (!source.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. - val string = readable.readAsString() + val string = source.readAsString() .getOrElse { when (it) { is DecodeError.Reading -> @@ -827,8 +831,8 @@ public object PdfMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(readable: Readable): Try { - readable.read(0L until 5L) + override suspend fun sniffBlob(source: Readable): Try { + source.read(0L until 5L) .getOrElse { error -> return Try.failure(MediaTypeSnifferError.Reading(error)) } @@ -850,12 +854,12 @@ public object JsonMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(readable: Readable): Try { - if (!readable.canReadWholeBlob()) { + override suspend fun sniffBlob(source: Readable): Try { + if (!source.canReadWholeBlob()) { return Try.failure(MediaTypeSnifferError.NotRecognized) } - readable.readAsJson() + source.readAsJson() .getOrElse { when (it) { is DecodeError.Reading -> @@ -893,8 +897,8 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(readable: Readable): Try { - readable.borrow() + override suspend fun sniffBlob(source: Readable): Try { + source.borrow() .asInputStream(wrapError = ::SystemSnifferException) .use { stream -> try { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt index 0e4c9e7e26..88bf96484e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt @@ -33,10 +33,10 @@ public object ZipMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - override suspend fun sniffBlob(readable: Readable): Try { - (readable as? Resource)?.source?.toFile() + override suspend fun sniffBlob(source: Readable): Try { + (source as? Resource)?.source?.toFile() ?.let { return fileZipArchiveProvider.sniffFile(it) } - return streamingZipArchiveProvider.sniffBlob(readable) + return streamingZipArchiveProvider.sniffBlob(source) } } From 1b05cea80ba7fb5818087db0194d0a0a1c9cb800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Fri, 1 Dec 2023 09:55:36 +0100 Subject: [PATCH 47/86] Move media type retriever back to `mediatype` package --- .../readium/r2/lcp/LcpPublicationRetriever.kt | 2 +- .../java/org/readium/r2/lcp/LcpService.kt | 2 +- .../readium/r2/lcp/service/LicensesService.kt | 2 +- .../readium/r2/lcp/service/NetworkService.kt | 2 +- .../r2/shared/util/asset/AssetRetriever.kt | 1 + .../r2/shared/util/content/ContentResource.kt | 2 +- .../util/content/ContentResourceFactory.kt | 3 ++- .../android/AndroidDownloadManager.kt | 2 +- .../DefaultMediaTypeSniffer.kt | 22 +------------------ .../MediaTypeRetriever.kt | 7 +----- .../SimpleResourceMediaTypeRetriever.kt | 7 +----- .../util/mediatype/MediaTypeRetrieverTest.kt | 2 -- .../readium/r2/streamer/PublicationFactory.kt | 4 ++-- .../r2/streamer/parser/audio/AudioParser.kt | 2 +- .../r2/streamer/parser/image/ImageParser.kt | 2 +- .../streamer/parser/image/ImageParserTest.kt | 4 ++-- .../java/org/readium/r2/testapp/Readium.kt | 4 ++-- 17 files changed, 20 insertions(+), 50 deletions(-) rename readium/shared/src/main/java/org/readium/r2/shared/util/{asset => mediatype}/DefaultMediaTypeSniffer.kt (56%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{asset => mediatype}/MediaTypeRetriever.kt (93%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{asset => mediatype}/SimpleResourceMediaTypeRetriever.kt (89%) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt index ee40c601d9..1b8e2b7a44 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt @@ -15,12 +15,12 @@ import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.ErrorException -import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse 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 /** * Utility to acquire a protected publication from an LCP License Document. diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt index 655f97b444..7cd9932196 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt @@ -30,9 +30,9 @@ import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeRetriever /** * Service used to acquire and open publications protected with LCP. diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index 8da051b4d0..72ecb65d98 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -37,11 +37,11 @@ import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import timber.log.Timber internal class LicensesService( diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt index 8b36e4f9a0..d5915a0145 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt @@ -23,12 +23,12 @@ import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.getOrElse 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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import timber.log.Timber diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index ad6f397e19..9ebcf8b1bc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -16,6 +16,7 @@ import org.readium.r2.shared.util.archive.SmartArchiveFactory import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceFactory import org.readium.r2.shared.util.toUrl diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt index 499bf2db27..0538fa2e18 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt @@ -161,5 +161,5 @@ public class ContentResource( } override fun toString(): String = - "${javaClass.simpleName}(${runBlocking { length() } } bytes )" + "${javaClass.simpleName}(${runBlocking { length() } } bytes)" } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt index 7d55c57cac..8ffac52585 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt @@ -15,7 +15,8 @@ import org.readium.r2.shared.util.resource.ResourceFactory import org.readium.r2.shared.util.toUri /** - * Creates [ContentResource]s from Urls. + * Creates [Resource] instances granting access to `content://` URLs provided by the given + * [contentResolver]. */ public class ContentResourceFactory( private val contentResolver: ContentResolver diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index 78aed0d9a0..dd831fb16a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.tryOr import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpError @@ -34,6 +33,7 @@ import org.readium.r2.shared.util.http.HttpStatus 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 import org.readium.r2.shared.util.toUri import org.readium.r2.shared.util.units.Hz import org.readium.r2.shared.util.units.hz diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/DefaultMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt similarity index 56% rename from readium/shared/src/main/java/org/readium/r2/shared/util/asset/DefaultMediaTypeSniffer.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt index 47940e8e11..b727de0cc2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/DefaultMediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt @@ -4,31 +4,11 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.asset +package org.readium.r2.shared.util.mediatype import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.Readable -import org.readium.r2.shared.util.mediatype.ArchiveMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.BitmapMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.CompositeMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.EpubMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.HtmlMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.JsonMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.LcpLicenseMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.LpfMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints -import org.readium.r2.shared.util.mediatype.MediaTypeSniffer -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.mediatype.OpdsMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.PdfMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.RarMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.SystemMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.W3cWpubMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.WebPubManifestMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.WebPubMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.XhtmlMediaTypeSniffer import org.readium.r2.shared.util.zip.ZipMediaTypeSniffer /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt similarity index 93% rename from readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt index 7b193859d7..574680bda4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.asset +package org.readium.r2.shared.util.mediatype import java.io.File import org.readium.r2.shared.util.Try @@ -14,11 +14,6 @@ import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.getOrElse -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.MediaTypeSniffer -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.use diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SimpleResourceMediaTypeRetriever.kt similarity index 89% rename from readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SimpleResourceMediaTypeRetriever.kt index 783b5e44ee..e2a7e71179 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SimpleResourceMediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SimpleResourceMediaTypeRetriever.kt @@ -4,16 +4,11 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.asset +package org.readium.r2.shared.util.mediatype import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse -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.MediaTypeSniffer -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.filename import org.readium.r2.shared.util.resource.mediaType diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt index a263007d91..4143f542d2 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt @@ -8,8 +8,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures import org.readium.r2.shared.util.assertSuccess -import org.readium.r2.shared.util.asset.DefaultMediaTypeSniffer -import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.resource.StringResource import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.robolectric.RobolectricTestRunner diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 3e08a517a3..915aa6ef05 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -15,13 +15,13 @@ import org.readium.r2.shared.publication.protection.LcpFallbackContentProtection import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.asset.DefaultMediaTypeSniffer -import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.logging.WarningLogger +import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer import org.readium.r2.shared.util.mediatype.FormatRegistry +import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.pdf.PdfDocumentFactory import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.readium.r2.streamer.parser.PublicationParser diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index 7d0b2c841c..b7da6ad8f9 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -14,11 +14,11 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index e54475de8b..835e8511b1 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -15,11 +15,11 @@ import org.readium.r2.shared.publication.services.PerResourcePositionsService import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index 9755ee6a1f..ff84d461ff 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -20,11 +20,11 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.firstWithRel import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.assertSuccess -import org.readium.r2.shared.util.asset.DefaultMediaTypeSniffer -import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.file.FileResource +import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.zip.ZipArchiveFactory diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 58c924b320..575db67327 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -18,14 +18,14 @@ import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetri import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.asset.DefaultMediaTypeSniffer -import org.readium.r2.shared.util.asset.MediaTypeRetriever import org.readium.r2.shared.util.content.ContentResourceFactory import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager import org.readium.r2.shared.util.file.FileResourceFactory import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpResourceFactory +import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer import org.readium.r2.shared.util.mediatype.FormatRegistry +import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.CompositeResourceFactory import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.readium.r2.streamer.PublicationFactory From 61db1b1c9c2658bd4876163a71093c47b8d70347 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Tue, 5 Dec 2023 17:27:41 +0100 Subject: [PATCH 48/86] Various fixes and cosmetic changes --- .../adapter/pdfium/document/PdfiumDocument.kt | 12 +--- .../pdfium/navigator/PdfiumEngineProvider.kt | 2 +- .../pspdfkit/document/PsPdfKitDocument.kt | 2 +- .../navigator/PsPdfKitEngineProvider.kt | 2 +- .../readium/r2/lcp/LcpContentProtection.kt | 24 ++++--- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 18 ++--- .../org/readium/r2/lcp/LcpDecryptorTest.kt | 10 +-- .../main/java/org/readium/r2/lcp/LcpError.kt | 4 +- .../lcp/license/container/LicenseContainer.kt | 6 +- .../readium/r2/lcp/service/LicensesService.kt | 6 +- .../r2/navigator/pdf/PdfEngineProvider.kt | 13 +++- .../r2/navigator/pdf/PdfNavigatorFragment.kt | 2 +- .../media/tts/TtsNavigatorFactory.kt | 8 +-- .../tts/android/AndroidTtsEngineProvider.kt | 4 +- .../media/tts/session/TtsSessionAdapter.kt | 2 +- .../r2/shared/publication/Properties.kt | 3 - .../r2/shared/publication/Publication.kt | 11 ++- .../AdeptFallbackContentProtection.kt | 11 +-- .../LcpFallbackContentProtection.kt | 12 ++-- .../publication/services/CoverService.kt | 69 ++----------------- .../iterators/HtmlResourceContentIterator.kt | 4 +- .../java/org/readium/r2/shared/util/Error.kt | 4 +- .../java/org/readium/r2/shared/util/Try.kt | 5 +- .../r2/shared/util/archive/ArchiveFactory.kt | 6 +- .../util/archive/SmartArchiveFactory.kt | 6 +- .../org/readium/r2/shared/util/asset/Asset.kt | 58 ++++++++-------- .../r2/shared/util/asset/AssetRetriever.kt | 12 ++-- .../util/content/ContentResolverError.kt | 4 +- .../r2/shared/util/content/ContentResource.kt | 8 +-- .../readium/r2/shared/util/data/Container.kt | 2 +- .../readium/r2/shared/util/data/Decoding.kt | 12 ++-- .../readium/r2/shared/util/data/ReadError.kt | 7 +- .../android/AndroidDownloadManager.kt | 10 +-- .../r2/shared/util/file/FileResource.kt | 4 +- .../r2/shared/util/http/DefaultHttpClient.kt | 30 ++++---- .../readium/r2/shared/util/http/HttpClient.kt | 2 +- .../readium/r2/shared/util/http/HttpError.kt | 2 +- .../r2/shared/util/http/HttpRequest.kt | 2 +- .../r2/shared/util/http/HttpResource.kt | 9 ++- .../r2/shared/util/http/HttpResponse.kt | 2 +- .../r2/shared/util/mediatype/MediaType.kt | 1 + .../shared/util/mediatype/MediaTypeSniffer.kt | 2 +- .../r2/shared/util/zip/FileZipContainer.kt | 4 +- .../shared/util/zip/StreamingZipContainer.kt | 4 +- .../r2/shared/util/zip/ZipArchiveFactory.kt | 6 +- .../shared/src/main/res/values/strings.xml | 18 ----- .../AdeptFallbackContentProtectionTest.kt | 14 ++-- .../LcpFallbackContentProtectionTest.kt | 15 ++-- .../util/mediatype/MediaTypeRetrieverTest.kt | 60 ++++++++-------- .../util/resource/BufferingResourceTest.kt | 4 +- .../util/resource/DirectoryContainerTest.kt | 4 +- .../shared/util/resource/ZipContainerTest.kt | 14 ++-- .../readium/r2/streamer/ParserAssetFactory.kt | 18 ++--- .../readium/r2/streamer/PublicationFactory.kt | 4 +- .../r2/streamer/parser/audio/AudioParser.kt | 4 +- .../r2/streamer/parser/epub/EpubParser.kt | 8 +-- .../r2/streamer/parser/image/ImageParser.kt | 4 +- .../r2/streamer/parser/pdf/PdfParser.kt | 4 +- .../parser/readium/ReadiumWebPubParser.kt | 6 +- .../parser/epub/EpubDeobfuscatorTest.kt | 8 +-- .../streamer/parser/image/ImageParserTest.kt | 4 +- .../java/org/readium/r2/testapp/Readium.kt | 4 +- .../catalogs/CatalogFeedListViewModel.kt | 4 +- .../readium/r2/testapp/domain/Bookshelf.kt | 4 +- .../r2/testapp/domain/PublicationRetriever.kt | 12 ++-- .../r2/testapp/domain/ReadUserError.kt | 2 +- .../r2/testapp/drm/DrmManagementViewModel.kt | 6 +- .../r2/testapp/reader/ReaderRepository.kt | 8 +-- 68 files changed, 288 insertions(+), 368 deletions(-) delete mode 100644 readium/shared/src/main/res/values/strings.xml diff --git a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt index 999440461d..21518d3314 100644 --- a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt +++ b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt @@ -18,10 +18,8 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.extensions.md5 import org.readium.r2.shared.extensions.tryOrNull -import org.readium.r2.shared.extensions.unwrapInstance import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.pdf.PdfDocument @@ -114,15 +112,7 @@ public class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory exception.error - else -> ReadError.Decoding("Pdfium could not read data.") - } - Try.failure(error) + Try.failure(ReadError.Decoding(e)) } } } diff --git a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt index 689c247e29..5d8fe18863 100644 --- a/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt +++ b/readium/adapters/pdfium/navigator/src/main/java/org/readium/adapter/pdfium/navigator/PdfiumEngineProvider.kt @@ -68,7 +68,7 @@ public class PdfiumEngineProvider( return settingsPolicy.settings(preferences) } - override fun computePresentation(settings: PdfiumSettings): OverflowableNavigator.Overflow = + override fun computeOverflow(settings: PdfiumSettings): OverflowableNavigator.Overflow = SimpleOverflow( readingProgression = settings.readingProgression, scroll = true, diff --git a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt index 6253e8cd4c..102a79b539 100644 --- a/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt +++ b/readium/adapters/pspdfkit/document/src/main/java/org/readium/adapter/pspdfkit/document/PsPdfKitDocument.kt @@ -38,7 +38,7 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory = withContext(Dispatchers.IO) { val dataProvider = ResourceDataProvider(resource) - val documentSource = DocumentSource(dataProvider) + val documentSource = DocumentSource(dataProvider, password) try { val innerDocument = PdfDocumentLoader.openDocument(context, documentSource) Try.success(PsPdfKitDocument(innerDocument)) diff --git a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt index dc6899443e..580748f6f3 100644 --- a/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt +++ b/readium/adapters/pspdfkit/navigator/src/main/java/org/readium/adapter/pspdfkit/navigator/PsPdfKitEngineProvider.kt @@ -68,7 +68,7 @@ public class PsPdfKitEngineProvider( return settingsPolicy.settings(preferences) } - override fun computePresentation(settings: PsPdfKitSettings): OverflowableNavigator.Overflow = + override fun computeOverflow(settings: PsPdfKitSettings): OverflowableNavigator.Overflow = SimpleOverflow( readingProgression = settings.readingProgression, scroll = settings.scroll, diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index dabc578ad2..84e975b637 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -13,11 +13,13 @@ 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.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse @@ -43,13 +45,13 @@ internal class LcpContentProtection( allowUserInteraction: Boolean ): Try { return when (asset) { - is Asset.Container -> openPublication(asset, credentials, allowUserInteraction) - is Asset.Resource -> openLicense(asset, credentials, allowUserInteraction) + is ContainerAsset -> openPublication(asset, credentials, allowUserInteraction) + is ResourceAsset -> openLicense(asset, credentials, allowUserInteraction) } } private suspend fun openPublication( - asset: Asset.Container, + asset: ContainerAsset, credentials: String?, allowUserInteraction: Boolean ): Try { @@ -70,7 +72,7 @@ internal class LcpContentProtection( } private fun createResultAsset( - asset: Asset.Container, + asset: ContainerAsset, license: Try ): Try { val serviceFactory = LcpContentProtectionService @@ -99,7 +101,7 @@ internal class LcpContentProtection( } private suspend fun openLicense( - licenseAsset: Asset.Resource, + licenseAsset: ResourceAsset, credentials: String?, allowUserInteraction: Boolean ): Try { @@ -114,7 +116,7 @@ internal class LcpContentProtection( return Try.failure( ContentProtection.Error.Reading( ReadError.Decoding( - MessageError( + DebugError( "Failed to read the LCP license document", cause = ThrowableError(e) ) @@ -134,7 +136,7 @@ internal class LcpContentProtection( ?: return Try.failure( ContentProtection.Error.Reading( ReadError.Decoding( - MessageError( + DebugError( "The LCP license document does not contain a valid link to the publication" ) ) @@ -147,18 +149,18 @@ internal class LcpContentProtection( url, mediaType = link.mediaType ) - .map { it as Asset.Container } + .map { it as ContainerAsset } .mapFailure { it.wrap() } } else { assetRetriever.retrieve(url) .mapFailure { it.wrap() } .flatMap { - if (it is Asset.Container) { + if (it is ContainerAsset) { Try.success((it)) } else { Try.failure( ContentProtection.Error.AssetNotSupported( - MessageError( + DebugError( "LCP license points to an unsupported publication." ) ) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index d4fd40e88b..61e06d4acb 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -13,8 +13,7 @@ import org.readium.r2.shared.extensions.coerceFirstNonNegative import org.readium.r2.shared.extensions.inflate import org.readium.r2.shared.extensions.requireLengthFitInt import org.readium.r2.shared.publication.encryption.Encryption -import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ReadError @@ -47,7 +46,7 @@ internal class LcpDecryptor( license == null -> FailureResource( ReadError.Decoding( - MessageError( + DebugError( "Cannot decipher content because the publication is locked." ) ) @@ -72,9 +71,6 @@ internal class LcpDecryptor( private val license: LcpLicense ) : TransformingResource(resource) { - override val source: AbsoluteUrl? = - null - override suspend fun transform(data: Try): Try = license.decryptFully(data, encryption.isDeflated) @@ -94,8 +90,6 @@ internal class LcpDecryptor( private val license: LcpLicense ) : Resource by resource { - override val source: AbsoluteUrl? = null - private class Cache( var startIndex: Int? = null, val data: ByteArray = ByteArray(3 * AES_BLOCK_SIZE) @@ -133,7 +127,7 @@ internal class LcpDecryptor( if (length < 2 * AES_BLOCK_SIZE) { return Try.failure( ReadError.Decoding( - MessageError("Invalid CBC-encrypted stream.") + DebugError("Invalid CBC-encrypted stream.") ) ) } @@ -146,7 +140,7 @@ internal class LcpDecryptor( .getOrElse { return Try.failure( ReadError.Decoding( - MessageError("Can't decrypt trailing size of CBC-encrypted stream") + DebugError("Can't decrypt trailing size of CBC-encrypted stream") ) ) } @@ -196,7 +190,7 @@ internal class LcpDecryptor( .getOrElse { return Try.failure( ReadError.Decoding( - MessageError( + DebugError( "Can't decrypt the content for resource with key: ${resource.source}", it ) @@ -261,7 +255,7 @@ private suspend fun LcpLicense.decryptFully( .getOrElse { return Try.failure( ReadError.Decoding( - MessageError("Failed to decrypt the resource", it) + DebugError("Failed to decrypt the resource", it) ) ) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt index ab23d09f72..5d4dbebdb9 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt @@ -16,7 +16,7 @@ import org.readium.r2.shared.extensions.coerceIn import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.getOrThrow import org.readium.r2.shared.util.resource.Resource @@ -51,7 +51,7 @@ internal suspend fun checkLengthComputationIsCorrect(publication: Publication) { (publication.readingOrder + publication.resources) .forEach { link -> - val trueLength = publication.get(link)!!.use { it.read().assertSuccess().size.toLong() } + val trueLength = publication.get(link)!!.use { it.read().checkSuccess().size.toLong() } publication.get(link)!!.use { resource -> resource.length() .onFailure { @@ -72,7 +72,7 @@ internal suspend fun checkAllResourcesAreReadableByChunks(publication: Publicati (publication.readingOrder + publication.resources) .forEach { link -> Timber.d("attempting to read ${link.href} by chunks ") - val groundTruth = publication.get(link)!!.use { it.read() }.assertSuccess() + val groundTruth = publication.get(link)!!.use { it.read() }.checkSuccess() for (chunkSize in listOf(4096L, 2050L)) { publication.get(link).use { resource -> resource!!.readByChunks(chunkSize, groundTruth).onFailure { @@ -92,8 +92,8 @@ internal suspend fun checkExceedingRangesAreAllowed(publication: Publication) { (publication.readingOrder + publication.resources) .forEach { link -> publication.get(link).use { resource -> - val length = resource!!.length().assertSuccess() - val fullTruth = resource.read().assertSuccess() + val length = resource!!.length().checkSuccess() + val fullTruth = resource.read().checkSuccess() for ( range in listOf( 0 until length + 100, diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt index ca61966568..158f24624c 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt @@ -9,9 +9,9 @@ package org.readium.r2.lcp import java.net.SocketTimeoutException import java.util.* import org.readium.r2.lcp.service.NetworkException +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ErrorException -import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Url @@ -46,7 +46,7 @@ public sealed class LcpError( * message and how to reproduce it. */ public class Runtime(message: String) : - LcpError("Unexpected LCP error", MessageError(message)) + LcpError("Unexpected LCP error", DebugError(message)) /** An unknown low-level exception was reported. */ public class Unknown(override val cause: Error?) : diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt index 5efad0fa99..65b7f2b60f 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt @@ -16,6 +16,8 @@ import org.readium.r2.lcp.LcpException import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource @@ -51,8 +53,8 @@ internal fun createLicenseContainer( asset: Asset ): LicenseContainer = when (asset) { - is Asset.Resource -> createLicenseContainer(asset.resource, asset.mediaType) - is Asset.Container -> createLicenseContainer(context, asset.container, asset.mediaType) + is ResourceAsset -> createLicenseContainer(asset.resource, asset.mediaType) + is ContainerAsset -> createLicenseContainer(context, asset.container, asset.mediaType) } internal fun createLicenseContainer( diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index 72ecb65d98..dd3c6445ab 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -37,6 +37,8 @@ import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry @@ -65,9 +67,9 @@ internal class LicensesService( override suspend fun isLcpProtected(asset: Asset): Boolean = tryOr(false) { when (asset) { - is Asset.Resource -> + is ResourceAsset -> asset.mediaType == MediaType.LCP_LICENSE_DOCUMENT - is Asset.Container -> { + is ContainerAsset -> { createLicenseContainer(context, asset.container, asset.mediaType).read() true } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt index bd715ee150..cfb12ce30a 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/pdf/PdfEngineProvider.kt @@ -10,7 +10,6 @@ import androidx.fragment.app.Fragment import kotlinx.coroutines.flow.StateFlow import org.readium.r2.navigator.Navigator import org.readium.r2.navigator.OverflowableNavigator -import org.readium.r2.navigator.VisualNavigator import org.readium.r2.navigator.input.InputListener import org.readium.r2.navigator.preferences.Configurable import org.readium.r2.navigator.preferences.PreferencesEditor @@ -38,10 +37,18 @@ public interface PdfEngineProvider get() = settings.mapStateIn(lifecycleScope) { settings -> - pdfEngineProvider.computePresentation(settings) + pdfEngineProvider.computeOverflow(settings) } @Deprecated( diff --git a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt index 5ed0ead3d9..27deebac6a 100644 --- a/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt +++ b/readium/navigators/media/tts/src/main/java/org/readium/navigator/media/tts/TtsNavigatorFactory.kt @@ -26,8 +26,8 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.content.Content import org.readium.r2.shared.publication.services.content.ContentService import org.readium.r2.shared.publication.services.content.content +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Language -import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.tokenizer.DefaultTextContentTokenizer @@ -140,7 +140,7 @@ public class TtsNavigatorFactory( val errorCode = when (error) { is ReadError.Access -> when (error.cause) { - is HttpError.Response -> + is HttpError.ErrorResponse -> ERROR_CODE_IO_BAD_HTTP_STATUS is HttpError.Timeout -> ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt index 024d02233a..8386db5121 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Properties.kt @@ -54,9 +54,6 @@ public data class Properties( */ public operator fun get(key: String): Any? = otherProperties[key] - internal fun toResourceProperties(): Properties = - Properties(otherProperties) - public companion object { /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index b89288c722..e5e7124c9c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -22,7 +22,6 @@ import org.readium.r2.shared.publication.services.CacheService import org.readium.r2.shared.publication.services.ContentProtectionService import org.readium.r2.shared.publication.services.CoverService import org.readium.r2.shared.publication.services.DefaultLocatorService -import org.readium.r2.shared.publication.services.ExternalCoverService import org.readium.r2.shared.publication.services.LocatorService import org.readium.r2.shared.publication.services.PositionsService import org.readium.r2.shared.publication.services.ResourceCoverService @@ -33,6 +32,7 @@ import org.readium.r2.shared.util.Closeable import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.EmptyContainer +import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.resource.Resource @@ -57,12 +57,13 @@ public typealias PublicationId = String * The default implementation returns Resource.Exception.NotFound for all HREFs. * @param servicesBuilder Holds the list of service factories used to create the instances of * Publication.Service attached to this Publication. + * @param httpClient An [HttpClient] to access remote services. */ public class Publication( public val manifest: Manifest, private val container: Container = EmptyContainer(), private val servicesBuilder: ServicesBuilder = ServicesBuilder(), - httpClient: HttpClient? = null, + httpClient: HttpClient = DefaultHttpClient(), @Deprecated( "Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR @@ -401,11 +402,7 @@ public class Publication( } if (!containsKey(CoverService::class.java.simpleName)) { - val factory = { context: Service.Context -> - ResourceCoverService.createFactory()(context) - ?: httpClient - ?.let { ExternalCoverService.createFactory(it)(context) } - } + val factory = ResourceCoverService.createFactory() put(CoverService::class.java.simpleName, factory) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt index 10a41f3556..979800c623 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt @@ -9,10 +9,11 @@ package org.readium.r2.shared.publication.protection import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.protection.ContentProtection.Scheme import org.readium.r2.shared.publication.services.contentProtectionServiceFactory -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.readAsXml @@ -29,7 +30,7 @@ public class AdeptFallbackContentProtection : ContentProtection { override val scheme: Scheme = Scheme.Adept override suspend fun supports(asset: Asset): Try { - if (asset !is Asset.Container) { + if (asset !is ContainerAsset) { return Try.success(false) } @@ -41,10 +42,10 @@ public class AdeptFallbackContentProtection : ContentProtection { credentials: String?, allowUserInteraction: Boolean ): Try { - if (asset !is Asset.Container) { + if (asset !is ContainerAsset) { return Try.failure( ContentProtection.Error.AssetNotSupported( - MessageError("A container asset was expected.") + DebugError("A container asset was expected.") ) ) } @@ -61,7 +62,7 @@ public class AdeptFallbackContentProtection : ContentProtection { return Try.success(protectedFile) } - private suspend fun isAdept(asset: Asset.Container): Try { + private suspend fun isAdept(asset: ContainerAsset): Try { if (!asset.mediaType.matches(MediaType.EPUB)) { return Try.success(false) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index e3a3cab4f4..9adef5671e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -10,10 +10,12 @@ import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.encryption.encryption import org.readium.r2.shared.publication.protection.ContentProtection.Scheme import org.readium.r2.shared.publication.services.contentProtectionServiceFactory -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError @@ -35,11 +37,11 @@ public class LcpFallbackContentProtection : ContentProtection { override suspend fun supports(asset: Asset): Try = when (asset) { - is Asset.Container -> isLcpProtected( + is ContainerAsset -> isLcpProtected( asset.container, asset.mediaType ) - is Asset.Resource -> + is ResourceAsset -> Try.success( asset.mediaType.matches(MediaType.LCP_LICENSE_DOCUMENT) ) @@ -50,10 +52,10 @@ public class LcpFallbackContentProtection : ContentProtection { credentials: String?, allowUserInteraction: Boolean ): Try { - if (asset !is Asset.Container) { + if (asset !is ContainerAsset) { return Try.failure( ContentProtection.Error.AssetNotSupported( - MessageError("A container asset was expected.") + DebugError("A container asset was expected.") ) ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index b39a3d0bf8..b063abd2ae 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -10,21 +10,15 @@ package org.readium.r2.shared.publication.services import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.util.Size import org.readium.r2.shared.extensions.scaleToFit import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.publication.firstWithRel -import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.readAsBitmap -import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpRequest -import org.readium.r2.shared.util.http.fetch import org.readium.r2.shared.util.resource.Resource /** @@ -59,65 +53,23 @@ public interface CoverService : Publication.Service { public suspend fun coverFitting(maxSize: Size): Bitmap? = cover()?.scaleToFit(maxSize) } -private suspend fun Publication.coverFromManifest(): Bitmap? { - for (link in linksWithRel("cover")) { - val data = get(link)?.read()?.getOrNull() ?: continue - return BitmapFactory.decodeByteArray(data, 0, data.size) ?: continue - } - return null -} - /** * Returns the publication cover as a [Bitmap] at its maximum size. */ -public suspend fun Publication.cover(): Bitmap? { +public suspend fun Publication.cover(): Bitmap? = findService(CoverService::class)?.cover()?.let { return it } - return coverFromManifest() -} /** * Returns the publication cover as a [Bitmap], scaled down to fit the given [maxSize]. */ -public suspend fun Publication.coverFitting(maxSize: Size): Bitmap? { +public suspend fun Publication.coverFitting(maxSize: Size): Bitmap? = findService(CoverService::class)?.coverFitting(maxSize)?.let { return it } - return coverFromManifest()?.scaleToFit(maxSize) -} /** Factory to build a [CoverService]. */ public var Publication.ServicesBuilder.coverServiceFactory: ServiceFactory? get() = get(CoverService::class) set(value) = set(CoverService::class, value) -internal class ExternalCoverService( - private val coverUrl: AbsoluteUrl, - private val httpClient: HttpClient -) : CoverService { - - override suspend fun cover(): Bitmap? { - val request = HttpRequest(coverUrl) - - val response = httpClient.fetch(request) - .getOrElse { return null } - - return BitmapFactory.decodeByteArray(response.body, 0, response.body.size) - } - - companion object { - - fun createFactory(httpClient: HttpClient): (Publication.Service.Context) -> ExternalCoverService? = { - val manifestUrl = it.manifest - .links - .firstWithRel("self") - ?.url() - - it.manifest - .linksWithRel("cover") - .firstNotNullOfOrNull { link -> link.url(base = manifestUrl) as? AbsoluteUrl } - ?.let { url -> ExternalCoverService(url, httpClient) } - } - } -} - internal class ResourceCoverService( private val coverUrl: Url, private val container: Container @@ -145,27 +97,16 @@ internal class ResourceCoverService( } } -/** - * A [CoverService] which provides a unique cover for each Publication. - */ -public abstract class GeneratedCoverService : CoverService { - abstract override suspend fun cover(): Bitmap -} - /** * A [CoverService] which uses a provided in-memory bitmap. */ -public class InMemoryCoverService internal constructor(private val cover: Bitmap) : GeneratedCoverService() { +public class InMemoryCoverService internal constructor(private val cover: Bitmap) : CoverService { public companion object { public fun createFactory(cover: Bitmap?): ServiceFactory = { - cover?.let { - InMemoryCoverService( - it - ) - } + cover?.let { InMemoryCoverService(it) } } } - override suspend fun cover(): Bitmap = cover + public override suspend fun cover(): Bitmap = cover } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt index a0869236d4..0dfec1b0c1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt @@ -31,8 +31,8 @@ import org.readium.r2.shared.publication.services.content.Content.ImageElement import org.readium.r2.shared.publication.services.content.Content.TextElement import org.readium.r2.shared.publication.services.content.Content.VideoElement import org.readium.r2.shared.publication.services.positionsByReadingOrder +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Language -import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.readAsString import org.readium.r2.shared.util.getOrElse @@ -156,7 +156,7 @@ public class HtmlResourceContentIterator internal constructor( withContext(Dispatchers.Default) { val document = resource.use { res -> val html = res.readAsString().getOrElse { - val error = MessageError("Failed to read HTML resource", it.cause) + val error = DebugError("Failed to read HTML resource", it.cause) Timber.w(error.toDebugDescription()) return@withContext ParsedElements() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt index fd50762d98..bf7a8bc18a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Error.kt @@ -23,9 +23,9 @@ public interface Error { } /** - * A basic [Error] implementation with a message. + * A basic [Error] implementation with a debug message. */ -public class MessageError( +public class DebugError( override val message: String, override val cause: Error? = null ) : Error diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt index 523e91d0ca..9739da74d5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Try.kt @@ -152,7 +152,10 @@ public inline fun Try.tryRecover(transform: (exception: F is Failure -> transform(value) } -public fun Try.assertSuccess(): S = +/** + * Returns value in case of success and throws an [IllegalStateException] in case of failure. + */ +public fun Try.checkSuccess(): S = when (this) { is Success -> value diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt index cc2354b94a..11cc15feaa 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt @@ -38,7 +38,7 @@ public interface ArchiveFactory { */ public suspend fun create( mediaType: MediaType, - readable: Readable + source: Readable ): Try, Error> } @@ -55,10 +55,10 @@ public class CompositeArchiveFactory( override suspend fun create( mediaType: MediaType, - readable: Readable + source: Readable ): Try, ArchiveFactory.Error> { for (factory in factories) { - factory.create(mediaType, readable) + factory.create(mediaType, source) .getOrElse { error -> when (error) { is ArchiveFactory.Error.FormatNotSupported -> null diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt index 51b0665a0a..72a8467b31 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt @@ -21,14 +21,14 @@ internal class SmartArchiveFactory( override suspend fun create( mediaType: MediaType, - readable: Readable + source: Readable ): Try, ArchiveFactory.Error> = - archiveFactory.create(mediaType, readable) + archiveFactory.create(mediaType, source) .tryRecover { error -> when (error) { is ArchiveFactory.Error.FormatNotSupported -> { formatRegistry.superType(mediaType) - ?.let { create(it, readable) } + ?.let { create(it, source) } ?: Try.failure(error) } is ArchiveFactory.Error.Reading -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt index a05e89c62e..45bef416fe 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt @@ -6,7 +6,9 @@ package org.readium.r2.shared.util.asset +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource /** * An asset which is either a single resource or a container that holds multiple resources. @@ -22,36 +24,36 @@ public sealed class Asset { * Releases in-memory resources related to this asset. */ public abstract suspend fun close() +} - /** - * A single resource asset. - * - * @param mediaType Media type of the asset. - * @param resource Opened resource to access the asset. - */ - public class Resource( - override val mediaType: MediaType, - public val resource: org.readium.r2.shared.util.resource.Resource - ) : Asset() { - - override suspend fun close() { - resource.close() - } +/** + * A container asset providing access to several resources. + * + * @param mediaType Media type of the asset. + * @param container Opened container to access asset resources. + */ +public class ContainerAsset( + override val mediaType: MediaType, + public val container: Container +) : Asset() { + + override suspend fun close() { + container.close() } +} - /** - * A container asset providing access to several resources. - * - * @param mediaType Media type of the asset. - * @param container Opened container to access asset resources. - */ - public class Container( - override val mediaType: MediaType, - public val container: org.readium.r2.shared.util.data.Container - ) : Asset() { - - override suspend fun close() { - container.close() - } +/** + * A single resource asset. + * + * @param mediaType Media type of the asset. + * @param resource Opened resource to access the asset. + */ +public class ResourceAsset( + override val mediaType: MediaType, + public val resource: Resource +) : Asset() { + + override suspend fun close() { + resource.close() } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 9ebcf8b1bc..db3f6bf379 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -8,7 +8,7 @@ package org.readium.r2.shared.util.asset import java.io.File import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.archive.ArchiveFactory @@ -68,11 +68,11 @@ public class AssetRetriever( is ArchiveFactory.Error.Reading -> Try.failure(Error.Reading(it.cause)) is ArchiveFactory.Error.FormatNotSupported -> - Try.success(Asset.Resource(mediaType, resource)) + Try.success(ResourceAsset(mediaType, resource)) } } - return Try.success(Asset.Container(mediaType, archive)) + return Try.success(ContainerAsset(mediaType, archive)) } private suspend fun retrieveResource( @@ -114,7 +114,7 @@ public class AssetRetriever( .getOrElse { return Try.failure( Error.FormatNotSupported( - MessageError("Cannot determine asset media type.") + DebugError("Cannot determine asset media type.") ) ) } @@ -125,10 +125,10 @@ public class AssetRetriever( is ArchiveFactory.Error.Reading -> return Try.failure(Error.Reading(it.cause)) is ArchiveFactory.Error.FormatNotSupported -> - return Try.success(Asset.Resource(mediaType, resource)) + return Try.success(ResourceAsset(mediaType, resource)) } } - return Try.success(Asset.Container(mediaType, container)) + return Try.success(ContainerAsset(mediaType, container)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt index c19cb95614..38e058eea7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResolverError.kt @@ -19,14 +19,14 @@ public sealed class ContentResolverError( ) : AccessError { public class FileNotFound( - cause: Error? + cause: Error? = null ) : ContentResolverError("File not found.", cause) { public constructor(exception: Exception) : this(ThrowableError(exception)) } public class NotAvailable( - cause: Error? + cause: Error? = null ) : ContentResolverError("Content Provider recently crashed.", cause) { public constructor(exception: Exception) : this(ThrowableError(exception)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt index 0538fa2e18..370baeb5ba 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap @@ -122,7 +122,7 @@ public class ContentResource( when (it) { null -> Try.failure( ReadError.UnsupportedOperation( - MessageError("Content provider does not provide length for uri $uri.") + DebugError("Content provider does not provide length for uri $uri.") ) ) else -> Try.success(it) @@ -138,9 +138,7 @@ public class ContentResource( val stream = contentResolver.openInputStream(uri) ?: return Try.failure( ReadError.Access( - ContentResolverError.NotAvailable( - MessageError("Content provider recently crashed.") - ) + ContentResolverError.NotAvailable() ) ) val result = block(stream) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index 70a5180243..c2d68a3e4c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -52,7 +52,7 @@ public class EmptyContainer : } /** - * Routes requests to child containers, depending on a provided predicate. + * Concatenates several containers. * * This can be used for example to serve a publication containing both local and remote resources, * and more generally to concatenate different content sources. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index c3d0a8ddf0..20e2094aa9 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -14,8 +14,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.flatMap @@ -85,14 +85,14 @@ public suspend fun Readable.readAsString( ): Try = read().decode( { String(it, charset = charset) }, - { MessageError("Content is not a valid $charset string.", ThrowableError(it)) } + { DebugError("Content is not a valid $charset string.", ThrowableError(it)) } ) /** Content as an XML document. */ public suspend fun Readable.readAsXml(): Try = read().decode( { XmlParser().parse(ByteArrayInputStream(it)) }, - { MessageError("Content is not a valid XML document.", ThrowableError(it)) } + { DebugError("Content is not a valid XML document.", ThrowableError(it)) } ) /** @@ -101,7 +101,7 @@ public suspend fun Readable.readAsXml(): Try = public suspend fun Readable.readAsJson(): Try = readAsString().decodeMap( { JSONObject(it) }, - { MessageError("Content is not valid JSON.", ThrowableError(it)) } + { DebugError("Content is not valid JSON.", ThrowableError(it)) } ) /** Readium Web Publication Manifest parsed from the content. */ @@ -111,7 +111,7 @@ public suspend fun Readable.readAsRwpm(): Try = ?.let { Try.success(it) } ?: Try.failure( DecodeError.Decoding( - MessageError("Content is not a valid RWPM.") + DebugError("Content is not a valid RWPM.") ) ) } @@ -127,7 +127,7 @@ public suspend fun Readable.readAsBitmap(): Try = ?.let { Try.success(it) } ?: Try.failure( DecodeError.Decoding( - MessageError("Could not decode resource as a bitmap.") + DebugError("Could not decode resource as a bitmap.") ) ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt index bffd677bf1..5dfba58c7b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt @@ -7,9 +7,9 @@ package org.readium.r2.shared.util.data import java.io.IOException +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ErrorException -import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try @@ -27,7 +27,7 @@ public sealed class ReadError( public class Decoding(cause: Error? = null) : ReadError("An error occurred while attempting to decode the content.", cause) { - public constructor(message: String) : this(MessageError(message)) + public constructor(message: String) : this(DebugError(message)) public constructor(exception: Exception) : this(ThrowableError(exception)) } @@ -40,7 +40,7 @@ public sealed class ReadError( public class UnsupportedOperation(cause: Error? = null) : ReadError("Could not proceed because an operation was not supported.", cause) { - public constructor(message: String) : this(MessageError(message)) + public constructor(message: String) : this(DebugError(message)) } } @@ -60,4 +60,5 @@ public interface AccessError : Error public class ReadException( public val error: ReadError ) : IOException(error.message, ErrorException(error)) + public typealias ReadTry = Try diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index dd831fb16a..a5074d94a1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -24,7 +24,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.tryOr -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.getOrElse @@ -294,7 +294,7 @@ public class AndroidDownloadManager internal constructor( } else { Try.failure( DownloadManager.Error.FileSystemError( - MessageError("Failed to rename the downloaded file.") + DebugError("Failed to rename the downloaded file.") ) ) } @@ -310,7 +310,7 @@ public class AndroidDownloadManager internal constructor( DownloadManager.Error.HttpError(HttpError.MalformedResponse(null)) SystemDownloadManager.ERROR_TOO_MANY_REDIRECTS -> DownloadManager.Error.HttpError( - HttpError.Redirection(MessageError("Too many redirects.")) + HttpError.Redirection(DebugError("Too many redirects.")) ) SystemDownloadManager.ERROR_CANNOT_RESUME -> DownloadManager.Error.CannotResume() @@ -328,8 +328,8 @@ public class AndroidDownloadManager internal constructor( private fun httpErrorForCode(code: Int): HttpError = when (code) { - in 0 until 1000 -> HttpError.Response(HttpStatus(code)) - else -> HttpError.MalformedResponse(MessageError("Unknown HTTP status code.")) + in 0 until 1000 -> HttpError.ErrorResponse(HttpStatus(code)) + else -> HttpError.MalformedResponse(DebugError("Unknown HTTP status code.")) } public override fun close() { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt index 9c3891830b..4d99d6d75f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.* import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrThrow @@ -107,7 +107,7 @@ public class FileResource( metadataLength?.let { Try.success(it) } ?: Try.failure( ReadError.UnsupportedOperation( - MessageError("Length not available for file at ${file.path}.") + DebugError("Length not available for file at ${file.path}.") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt index 3764547268..de84072498 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/DefaultHttpClient.kt @@ -22,8 +22,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.joinValues import org.readium.r2.shared.extensions.lowerCaseKeys -import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url @@ -121,7 +120,7 @@ public class DefaultHttpClient( ): HttpTry = Try.failure( HttpError.Redirection( - MessageError("Request cancelled because of an unsafe redirect.") + DebugError("Request cancelled because of an unsafe redirect.") ) ) @@ -171,18 +170,18 @@ public class DefaultHttpClient( // JSON Problem Details or OPDS Authentication Document val body = connection.errorStream?.use { it.readBytes() } - val mediaType = MediaType(connection.contentType) + val mediaType = connection.contentType?.let { MediaType(it) } return@withContext Try.failure( - HttpError.Response(HttpStatus(statusCode), mediaType, body) + HttpError.ErrorResponse(HttpStatus(statusCode), mediaType, body) ) } val response = HttpResponse( request = request, url = request.url, - statusCode = statusCode, + statusCode = HttpStatus(statusCode), headers = connection.safeHeaders, - mediaType = MediaType(connection.contentType) + mediaType = connection.contentType?.let { MediaType(it) } ) callback.onResponseReceived(request, response) @@ -205,16 +204,12 @@ public class DefaultHttpClient( return callback.onStartRequest(request) .flatMap { tryStream(it) } .tryRecover { error -> - if (error !is HttpError.Redirection) { - callback.onRecoverRequest(request, error) - .flatMap { stream(it) } - } else { - Try.failure(error) - } + callback.onRecoverRequest(request, error) + .flatMap { stream(it) } } .onFailure { callback.onRequestFailed(request, it) - val error = MessageError("HTTP request failed ${request.url}", it) + val error = DebugError("HTTP request failed ${request.url}", it) Timber.e(error.toDebugDescription()) } } @@ -236,16 +231,17 @@ public class DefaultHttpClient( if (redirectCount > 5) { return Try.failure( HttpError.Redirection( - MessageError("There were too many redirects to follow.") + DebugError("There were too many redirects to follow.") ) ) } val location = response.header("Location") - ?.let { Url(it)?.resolve(request.url) as? AbsoluteUrl } + ?.let { Url(it) } + ?.let { request.url.resolve(it) } ?: return Try.failure( HttpError.MalformedResponse( - MessageError("Location of redirect is missing or invalid.") + DebugError("Location of redirect is missing or invalid.") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt index ad9fdd2fa1..e3940012b3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpClient.kt @@ -133,7 +133,7 @@ public suspend fun HttpClient.head(request: HttpRequest): HttpTry .copy { method = HttpRequest.Method.HEAD } .response() .tryRecover { error -> - if (error !is HttpError.Response || error.status != HttpStatus.MethodNotAllowed) { + if (error !is HttpError.ErrorResponse || error.status != HttpStatus.MethodNotAllowed) { return@tryRecover Try.failure(error) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt index 27b4dd4e6a..23ba24bcca 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpError.kt @@ -46,7 +46,7 @@ public sealed class HttpError( * @param mediaType Response media type. * @param body Response body. */ - public class Response( + public class ErrorResponse( public val status: HttpStatus, public val mediaType: MediaType? = null, public val body: ByteArray? = null diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt index 499874240b..1088b8ca44 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt @@ -73,7 +73,7 @@ public class HttpRequest( } public class Builder( - public var url: AbsoluteUrl, + public val url: AbsoluteUrl, public var method: Method = Method.GET, public var headers: MutableMap> = mutableMapOf(), public var body: Body? = null, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index bd1bc9abf4..c0c8adb51c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -14,7 +14,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.extensions.read import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap @@ -50,7 +50,7 @@ public class HttpResource( } else { Try.failure( ReadError.UnsupportedOperation( - MessageError( + DebugError( "Server did not provide content length in its response to request to $source." ) ) @@ -116,9 +116,8 @@ public class HttpResource( return client.stream(request) .mapFailure { ReadError.Access(it) } .flatMap { response -> - if (from != null && response.response.statusCode != 206 - ) { - val error = MessageError( + if (from != null && response.response.statusCode.code != 206) { + val error = DebugError( "Server seems not to support range requests to $source." ) Try.failure(ReadError.UnsupportedOperation(error)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt index 7cb4801e66..a13c03ec12 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResponse.kt @@ -21,7 +21,7 @@ import org.readium.r2.shared.util.mediatype.MediaType public data class HttpResponse( val request: HttpRequest, val url: AbsoluteUrl, - val statusCode: Int, + val statusCode: HttpStatus, val headers: Map>, val mediaType: MediaType? ) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt index a7389d6053..a7bad23c64 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt @@ -200,6 +200,7 @@ public class MediaType private constructor( public val isRpf: Boolean get() = matchesAny( READIUM_WEBPUB, READIUM_AUDIOBOOK, + DIVINA, LCP_PROTECTED_PDF, LCP_PROTECTED_AUDIOBOOK ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index cbda2349cd..a824a6cde7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -693,7 +693,7 @@ public object RarMediaTypeSniffer : MediaTypeSniffer { hints.hasMediaType("application/x-rar") || hints.hasMediaType("application/x-rar-compressed") ) { - return Try.success(MediaType.LPF) + return Try.success(MediaType.RAR) } return Try.failure(MediaTypeSnifferError.NotRecognized) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt index a72dfa2c84..8bd703b9cf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.readFully import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url @@ -57,7 +57,7 @@ internal class FileZipContainer( ?.let { Try.success(it) } ?: Try.failure( ReadError.UnsupportedOperation( - MessageError("ZIP entry doesn't provide length for entry $url.") + DebugError("ZIP entry doesn't provide length for entry $url.") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt index 4ed26c1812..37615aa6ee 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt @@ -12,7 +12,7 @@ import org.readium.r2.shared.extensions.readFully import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.extensions.unwrapInstance import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url @@ -59,7 +59,7 @@ internal class StreamingZipContainer( ?.let { Try.success(it) } ?: Try.failure( ReadError.UnsupportedOperation( - MessageError("ZIP entry doesn't provide length for entry $url.") + DebugError("ZIP entry doesn't provide length for entry $url.") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt index 30c43f7531..cc2117c93d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt @@ -21,9 +21,9 @@ public class ZipArchiveFactory : ArchiveFactory { override suspend fun create( mediaType: MediaType, - readable: Readable + source: Readable ): Try, ArchiveFactory.Error> = - (readable as? Resource)?.source?.toFile() + (source as? Resource)?.source?.toFile() ?.let { fileZipArchiveProvider.create(mediaType, it) } - ?: streamingZipArchiveProvider.create(mediaType, readable) + ?: streamingZipArchiveProvider.create(mediaType, source) } diff --git a/readium/shared/src/main/res/values/strings.xml b/readium/shared/src/main/res/values/strings.xml deleted file mode 100644 index 69de7009d7..0000000000 --- a/readium/shared/src/main/res/values/strings.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Format not supported - File not found - The file is corrupted and can\'t be opened - You are not allowed to open this publication - Not available, please try again later - Provided credentials were incorrect - - This publication cannot be opened because it is protected with %1$s - This publication cannot be opened because it is protected with an unknown DRM - \ No newline at end of file diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtectionTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtectionTest.kt index 3ec624a9d6..be8f74fdc3 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtectionTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtectionTest.kt @@ -13,8 +13,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.assertSuccess -import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @@ -24,7 +24,7 @@ class AdeptFallbackContentProtectionTest { @Test fun `Sniff no content protection`() { - assertFalse(supports(mediaType = MediaType.EPUB, resources = emptyMap()).assertSuccess()) + assertFalse(supports(mediaType = MediaType.EPUB, resources = emptyMap()).checkSuccess()) } @Test @@ -35,7 +35,7 @@ class AdeptFallbackContentProtectionTest { resources = mapOf( "META-INF/encryption.xml" to """""" ) - ).assertSuccess() + ).checkSuccess() ) } @@ -57,7 +57,7 @@ class AdeptFallbackContentProtectionTest { """ ) - ).assertSuccess() + ).checkSuccess() ) } @@ -70,13 +70,13 @@ class AdeptFallbackContentProtectionTest { "META-INF/encryption.xml" to """""", "META-INF/rights.xml" to """""" ) - ).assertSuccess() + ).checkSuccess() ) } private fun supports(mediaType: MediaType, resources: Map): Try = runBlocking { AdeptFallbackContentProtection().supports( - Asset.Container( + ContainerAsset( mediaType = mediaType, container = TestContainer(resources.mapKeys { Url(it.key)!! }) ) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt index 5fc4853ead..ce614c9fb9 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt @@ -13,7 +13,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @@ -23,7 +24,7 @@ class LcpFallbackContentProtectionTest { @Test fun `Sniff no content protection`() { - assertFalse(supports(mediaType = MediaType.EPUB, resources = emptyMap()).assertSuccess()) + assertFalse(supports(mediaType = MediaType.EPUB, resources = emptyMap()).checkSuccess()) } @Test @@ -34,7 +35,7 @@ class LcpFallbackContentProtectionTest { resources = mapOf( "META-INF/encryption.xml" to """""" ) - ).assertSuccess() + ).checkSuccess() ) } @@ -46,7 +47,7 @@ class LcpFallbackContentProtectionTest { resources = mapOf( "license.lcpl" to "{}" ) - ).assertSuccess() + ).checkSuccess() ) } @@ -58,7 +59,7 @@ class LcpFallbackContentProtectionTest { resources = mapOf( "META-INF/license.lcpl" to "{}" ) - ).assertSuccess() + ).checkSuccess() ) } @@ -86,13 +87,13 @@ class LcpFallbackContentProtectionTest { """ ) - ).assertSuccess() + ).checkSuccess() ) } private fun supports(mediaType: MediaType, resources: Map): Try = runBlocking { LcpFallbackContentProtection().supports( - org.readium.r2.shared.util.asset.Asset.Container( + ContainerAsset( mediaType = mediaType, container = TestContainer(resources.mapKeys { Url(it.key)!! }) ) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt index 4143f542d2..35d6b8059f 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures -import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.resource.StringResource import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.robolectric.RobolectricTestRunner @@ -81,7 +81,7 @@ class MediaTypeRetrieverTest { fun `sniff from bytes`() = runBlocking { assertEquals( MediaType.READIUM_AUDIOBOOK_MANIFEST, - retriever.retrieve(fixtures.fileAt("audiobook.json")).assertSuccess() + retriever.retrieve(fixtures.fileAt("audiobook.json")).checkSuccess() ) } @@ -106,7 +106,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.READIUM_AUDIOBOOK, - retriever.retrieve(fixtures.fileAt("audiobook-package.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("audiobook-package.unknown")).checkSuccess() ) } @@ -118,11 +118,11 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.READIUM_AUDIOBOOK_MANIFEST, - retriever.retrieve(fixtures.fileAt("audiobook.json")).assertSuccess() + retriever.retrieve(fixtures.fileAt("audiobook.json")).checkSuccess() ) assertEquals( MediaType.READIUM_AUDIOBOOK_MANIFEST, - retriever.retrieve(fixtures.fileAt("audiobook-wrongtype.json")).assertSuccess() + retriever.retrieve(fixtures.fileAt("audiobook-wrongtype.json")).checkSuccess() ) } @@ -146,7 +146,7 @@ class MediaTypeRetrieverTest { assertEquals( MediaType.CBZ, - retriever.retrieve(fixtures.fileAt("cbz.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("cbz.unknown")).checkSuccess() ) } @@ -159,7 +159,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.DIVINA, - retriever.retrieve(fixtures.fileAt("divina-package.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("divina-package.unknown")).checkSuccess() ) } @@ -171,7 +171,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.DIVINA_MANIFEST, - retriever.retrieve(fixtures.fileAt("divina.json")).assertSuccess() + retriever.retrieve(fixtures.fileAt("divina.json")).checkSuccess() ) } @@ -184,7 +184,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.EPUB, - retriever.retrieve(fixtures.fileAt("epub.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("epub.unknown")).checkSuccess() ) } @@ -207,11 +207,11 @@ class MediaTypeRetrieverTest { assertEquals(MediaType.HTML, retriever.retrieve(mediaType = "text/html")) assertEquals( MediaType.HTML, - retriever.retrieve(fixtures.fileAt("html.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("html.unknown")).checkSuccess() ) assertEquals( MediaType.HTML, - retriever.retrieve(fixtures.fileAt("html-doctype-case.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("html-doctype-case.unknown")).checkSuccess() ) } @@ -225,7 +225,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.XHTML, - retriever.retrieve(fixtures.fileAt("xhtml.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("xhtml.unknown")).checkSuccess() ) } @@ -262,7 +262,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.OPDS1, - retriever.retrieve(fixtures.fileAt("opds1-feed.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("opds1-feed.unknown")).checkSuccess() ) } @@ -276,7 +276,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.OPDS1_ENTRY, - retriever.retrieve(fixtures.fileAt("opds1-entry.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("opds1-entry.unknown")).checkSuccess() ) } @@ -288,7 +288,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.OPDS2, - retriever.retrieve(fixtures.fileAt("opds2-feed.json")).assertSuccess() + retriever.retrieve(fixtures.fileAt("opds2-feed.json")).checkSuccess() ) } @@ -300,7 +300,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.OPDS2_PUBLICATION, - retriever.retrieve(fixtures.fileAt("opds2-publication.json")).assertSuccess() + retriever.retrieve(fixtures.fileAt("opds2-publication.json")).checkSuccess() ) } @@ -316,7 +316,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.OPDS_AUTHENTICATION, - retriever.retrieve(fixtures.fileAt("opds-authentication.json")).assertSuccess() + retriever.retrieve(fixtures.fileAt("opds-authentication.json")).checkSuccess() ) } @@ -332,7 +332,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.LCP_PROTECTED_AUDIOBOOK, - retriever.retrieve(fixtures.fileAt("audiobook-lcp.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("audiobook-lcp.unknown")).checkSuccess() ) } @@ -348,7 +348,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.LCP_PROTECTED_PDF, - retriever.retrieve(fixtures.fileAt("pdf-lcp.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("pdf-lcp.unknown")).checkSuccess() ) } @@ -364,7 +364,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.LCP_LICENSE_DOCUMENT, - retriever.retrieve(fixtures.fileAt("lcpl.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("lcpl.unknown")).checkSuccess() ) } @@ -374,11 +374,11 @@ class MediaTypeRetrieverTest { assertEquals(MediaType.LPF, retriever.retrieve(mediaType = "application/lpf+zip")) assertEquals( MediaType.LPF, - retriever.retrieve(fixtures.fileAt("lpf.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("lpf.unknown")).checkSuccess() ) assertEquals( MediaType.LPF, - retriever.retrieve(fixtures.fileAt("lpf-index-html.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("lpf-index-html.unknown")).checkSuccess() ) } @@ -388,7 +388,7 @@ class MediaTypeRetrieverTest { assertEquals(MediaType.PDF, retriever.retrieve(mediaType = "application/pdf")) assertEquals( MediaType.PDF, - retriever.retrieve(fixtures.fileAt("pdf.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("pdf.unknown")).checkSuccess() ) } @@ -424,7 +424,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.READIUM_WEBPUB, - retriever.retrieve(fixtures.fileAt("webpub-package.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("webpub-package.unknown")).checkSuccess() ) } @@ -436,7 +436,7 @@ class MediaTypeRetrieverTest { ) assertEquals( MediaType.READIUM_WEBPUB_MANIFEST, - retriever.retrieve(fixtures.fileAt("webpub.json")).assertSuccess() + retriever.retrieve(fixtures.fileAt("webpub.json")).checkSuccess() ) } @@ -444,7 +444,7 @@ class MediaTypeRetrieverTest { fun `sniff W3C WPUB manifest`() = runBlocking { assertEquals( MediaType.W3C_WPUB_MANIFEST, - retriever.retrieve(fixtures.fileAt("w3c-wpub.json")).assertSuccess() + retriever.retrieve(fixtures.fileAt("w3c-wpub.json")).checkSuccess() ) } @@ -453,7 +453,7 @@ class MediaTypeRetrieverTest { assertEquals(MediaType.ZAB, retriever.retrieve(fileExtension = "zab")) assertEquals( MediaType.ZAB, - retriever.retrieve(fixtures.fileAt("zab.unknown")).assertSuccess() + retriever.retrieve(fixtures.fileAt("zab.unknown")).checkSuccess() ) } @@ -461,7 +461,7 @@ class MediaTypeRetrieverTest { fun `sniff JSON`() = runBlocking { assertEquals( MediaType.JSON, - retriever.retrieve(fixtures.fileAt("any.json")).assertSuccess() + retriever.retrieve(fixtures.fileAt("any.json")).checkSuccess() ) } @@ -482,7 +482,7 @@ class MediaTypeRetrieverTest { retriever.retrieve( resource = StringResource("""{"title": "Message"}"""), hints = MediaTypeHints(mediaType = MediaType("application/problem+json")!!) - ).assertSuccess() + ).checkSuccess() ) } @@ -516,6 +516,6 @@ class MediaTypeRetrieverTest { fun `sniff system media types from bytes`() = runBlocking { shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("png", "image/png") val png = MediaType("image/png")!! - assertEquals(png, retriever.retrieve(fixtures.fileAt("png.unknown")).assertSuccess()) + assertEquals(png, retriever.retrieve(fixtures.fileAt("png.unknown")).checkSuccess()) } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt index 8a5386d63c..d1fa49dc8b 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures -import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.mediatype.MediaType import org.robolectric.RobolectricTestRunner @@ -22,7 +22,7 @@ class BufferingResourceTest { @Test fun `get properties`() = runBlocking { - assertEquals(resource.properties().assertSuccess(), sut().properties().assertSuccess()) + assertEquals(resource.properties().checkSuccess(), sut().properties().checkSuccess()) } @Test diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt index a359aff57c..cdd73e6399 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/DirectoryContainerTest.kt @@ -21,7 +21,7 @@ import org.junit.runner.RunWith import org.readium.r2.shared.lengthBlocking import org.readium.r2.shared.readBlocking import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.file.DirectoryContainer import org.readium.r2.shared.util.toAbsoluteUrl @@ -37,7 +37,7 @@ class DirectoryContainerTest { private fun sut(): Container = runBlocking { assertNotNull( - DirectoryContainer(directory).assertSuccess() + DirectoryContainer(directory).checkSuccess() ) } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt index 33f60d9a87..0fca54d1b1 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -19,7 +19,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.file.DirectoryContainer import org.readium.r2.shared.util.file.FileResource @@ -60,7 +60,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { assertNotNull(epubExploded) val explodedArchive = suspend { assertNotNull( - DirectoryContainer(File(epubExploded.path)).assertSuccess() + DirectoryContainer(File(epubExploded.path)).checkSuccess() ) } assertNotNull(explodedArchive) @@ -99,7 +99,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { fun `Fully reading an entry works well`(): Unit = runBlocking { sut().use { container -> val resource = assertNotNull(container[Url("mimetype")!!]) - val bytes = resource.read().assertSuccess() + val bytes = resource.read().checkSuccess() assertEquals("application/epub+zip", bytes.toString(StandardCharsets.UTF_8)) } } @@ -108,7 +108,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { fun `Reading a range of an entry works well`(): Unit = runBlocking { sut().use { container -> val resource = assertNotNull(container[Url("mimetype")!!]) - val bytes = resource.read(0..10L).assertSuccess() + val bytes = resource.read(0..10L).checkSuccess() assertEquals("application", bytes.toString(StandardCharsets.UTF_8)) assertEquals(11, bytes.size) } @@ -118,7 +118,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { fun `Out of range indexes are clamped to the available length`(): Unit = runBlocking { sut().use { container -> val resource = assertNotNull(container[Url("mimetype")!!]) - val bytes = resource.read(-5..60L).assertSuccess() + val bytes = resource.read(-5..60L).checkSuccess() assertEquals("application/epub+zip", bytes.toString(StandardCharsets.UTF_8)) assertEquals(20, bytes.size) } @@ -128,7 +128,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { fun `Decreasing ranges are understood as empty ones`(): Unit = runBlocking { sut().use { container -> val resource = assertNotNull(container[Url("mimetype")!!]) - val bytes = resource.read(60..20L).assertSuccess() + val bytes = resource.read(60..20L).checkSuccess() assertEquals("", bytes.toString(StandardCharsets.UTF_8)) assertEquals(0, bytes.size) } @@ -138,7 +138,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { fun `Computing size works well`(): Unit = runBlocking { sut().use { container -> val resource = assertNotNull(container[Url("mimetype")!!]) - val size = resource.length().assertSuccess() + val size = resource.length().checkSuccess() assertEquals(20L, size) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index ae6e3c2970..4132c476c4 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -8,10 +8,12 @@ package org.readium.r2.streamer import org.readium.r2.shared.extensions.addPrefix import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.data.CompositeContainer import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError @@ -48,15 +50,15 @@ internal class ParserAssetFactory( asset: Asset ): Try { return when (asset) { - is Asset.Container -> + is ContainerAsset -> createParserAssetForContainer(asset) - is Asset.Resource -> + is ResourceAsset -> createParserAssetForResource(asset) } } private fun createParserAssetForContainer( - asset: Asset.Container + asset: ContainerAsset ): Try = Try.success( PublicationParser.Asset( @@ -66,7 +68,7 @@ internal class ParserAssetFactory( ) private suspend fun createParserAssetForResource( - asset: Asset.Resource + asset: ResourceAsset ): Try = if (asset.mediaType.isRwpm) { createParserAssetForManifest(asset) @@ -75,7 +77,7 @@ internal class ParserAssetFactory( } private suspend fun createParserAssetForManifest( - asset: Asset.Resource + asset: ResourceAsset ): Try { val manifest = asset.resource.readAsRwpm() .mapFailure { @@ -100,7 +102,7 @@ internal class ParserAssetFactory( if (!baseUrl.isHttp) { return Try.failure( Error.UnsupportedAsset( - MessageError("Self link doesn't use the HTTP(S) scheme.") + DebugError("Self link doesn't use the HTTP(S) scheme.") ) ) } @@ -128,7 +130,7 @@ internal class ParserAssetFactory( } private fun createParserAssetForContent( - asset: Asset.Resource + asset: ResourceAsset ): Try { // Historically, the reading order of a standalone file contained a single link with the // HREF "/$assetName". This was fragile if the asset named changed, or was different on diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 915aa6ef05..02543e513f 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -12,7 +12,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.protection.AdeptFallbackContentProtection import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.publication.protection.LcpFallbackContentProtection -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.getOrElse @@ -275,7 +275,7 @@ public class PublicationFactory( private fun wrapParserException(e: PublicationParser.Error): Error = when (e) { is PublicationParser.Error.FormatNotSupported -> - Error.FormatNotSupported(MessageError("Cannot find a parser for this asset.")) + Error.FormatNotSupported(DebugError("Cannot find a parser for this asset.")) is PublicationParser.Error.Reading -> Error.Reading(e.cause) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index b7da6ad8f9..d65b7079e1 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -11,7 +11,7 @@ import org.readium.r2.shared.publication.LocalizedString import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Metadata import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ReadError @@ -58,7 +58,7 @@ public class AudioParser( return Try.failure( PublicationParser.Error.Reading( ReadError.Decoding( - MessageError("No audio file found in the publication.") + DebugError("No audio file found in the publication.") ) ) ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index bf16e31d41..5e384b39f4 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -14,7 +14,7 @@ import org.readium.r2.shared.publication.encryption.encryption import org.readium.r2.shared.publication.services.content.DefaultContentService import org.readium.r2.shared.publication.services.content.iterators.HtmlResourceContentIterator import org.readium.r2.shared.publication.services.search.StringSearchService -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container @@ -56,7 +56,7 @@ public class EpubParser( ?: return Try.failure( PublicationParser.Error.Reading( ReadError.Decoding( - MessageError("Missing OPF file.") + DebugError("Missing OPF file.") ) ) ) @@ -67,7 +67,7 @@ public class EpubParser( ?: return Try.failure( PublicationParser.Error.Reading( ReadError.Decoding( - MessageError("Invalid OPF file.") + DebugError("Invalid OPF file.") ) ) ) @@ -202,7 +202,7 @@ public class EpubParser( is DecodeError.Decoding -> PublicationParser.Error.Reading( ReadError.Decoding( - MessageError( + DebugError( "Couldn't decode resource at $url", it.cause ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index 835e8511b1..8d27bbf22e 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -12,7 +12,7 @@ import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Metadata import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.PerResourcePositionsService -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ReadError @@ -57,7 +57,7 @@ public class ImageParser( return Try.failure( PublicationParser.Error.Reading( ReadError.Decoding( - MessageError("No bitmap found in the publication.") + DebugError("No bitmap found in the publication.") ) ) ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index 5332c30399..f9e8f9faa9 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -12,7 +12,7 @@ import org.readium.r2.shared.PdfSupport import org.readium.r2.shared.publication.* import org.readium.r2.shared.publication.services.InMemoryCacheService import org.readium.r2.shared.publication.services.InMemoryCoverService -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.getOrElse @@ -50,7 +50,7 @@ public class PdfParser( ?: return Try.failure( PublicationParser.Error.Reading( ReadError.Decoding( - MessageError("No PDF found in the publication.") + DebugError("No PDF found in the publication.") ) ) ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index ed8317c1a5..b39eb7e523 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -13,7 +13,7 @@ import org.readium.r2.shared.publication.services.PerResourcePositionsService import org.readium.r2.shared.publication.services.cacheServiceFactory import org.readium.r2.shared.publication.services.locatorServiceFactory import org.readium.r2.shared.publication.services.positionsServiceFactory -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.DecodeError @@ -46,7 +46,7 @@ public class ReadiumWebPubParser( ?: return Try.failure( PublicationParser.Error.Reading( ReadError.Decoding( - MessageError("Missing manifest.") + DebugError("Missing manifest.") ) ) ) @@ -66,7 +66,7 @@ public class ReadiumWebPubParser( return Try.failure( PublicationParser.Error.Reading( ReadError.Decoding( - MessageError("Failed to parse the RWPM Manifest.") + DebugError("Failed to parse the RWPM Manifest.") ) ) ) diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt index 561079c37a..1fd8ab17aa 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt @@ -17,7 +17,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.file.DirectoryContainer import org.readium.r2.shared.util.resource.Resource import org.readium.r2.streamer.readBlocking @@ -36,12 +36,12 @@ class EpubDeobfuscatorTest { ) private val container = runBlocking { - DirectoryContainer(deobfuscationDir).assertSuccess() + DirectoryContainer(deobfuscationDir).checkSuccess() } private val font = requireNotNull(container[Url("cut-cut.woff")!!]) .readBlocking() - .assertSuccess() + .checkSuccess() private fun deobfuscate(url: Url, resource: Resource, algorithm: String?): Resource { val deobfuscator = EpubDeobfuscator(identifier) { @@ -78,7 +78,7 @@ class EpubDeobfuscatorTest { url, resource, "http://www.idpf.org/2008/embedding" - ).read(20L until 40L).assertSuccess() + ).read(20L until 40L).checkSuccess() assertThat(deobfuscatedRes).isEqualTo(font.copyOfRange(20, 40)) } } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index ff84d461ff..ca6ebe942f 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -19,7 +19,7 @@ import org.junit.runner.RunWith import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.firstWithRel import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.assertSuccess +import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer import org.readium.r2.shared.util.mediatype.FormatRegistry @@ -47,7 +47,7 @@ class ImageParserTest { private val cbzAsset = runBlocking { val file = fileForResource("futuristic_tales.cbz") val resource = FileResource(file, mediaType = MediaType.CBZ) - val archive = ZipArchiveFactory().create(MediaType.ZIP, resource).assertSuccess() + val archive = ZipArchiveFactory().create(MediaType.ZIP, resource).checkSuccess() PublicationParser.Asset(mediaType = MediaType.CBZ, archive) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 575db67327..a402d07402 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -15,7 +15,7 @@ import org.readium.r2.lcp.auth.LcpDialogAuthentication import org.readium.r2.navigator.preferences.FontFamily import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetriever -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.content.ContentResourceFactory @@ -83,7 +83,7 @@ class Readium(context: Context) { mediaTypeRetriever, downloadManager )?.let { Try.success(it) } - ?: Try.failure(LcpError.Unknown(MessageError("liblcp is missing on the classpath"))) + ?: Try.failure(LcpError.Unknown(DebugError("liblcp is missing on the classpath"))) private val lcpDialogAuthentication = LcpDialogAuthentication() diff --git a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt index 1dec3fa22f..e503660a35 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/catalogs/CatalogFeedListViewModel.kt @@ -16,8 +16,8 @@ import org.readium.r2.opds.OPDS1Parser import org.readium.r2.opds.OPDS2Parser import org.readium.r2.shared.opds.ParseData import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.fetchWithDecoder @@ -60,7 +60,7 @@ class CatalogFeedListViewModel(application: Application) : AndroidViewModel(appl private suspend fun parseURL(urlString: String): Try { val url = AbsoluteUrl(urlString) - ?: return Try.failure(MessageError("Invalid URL")) + ?: return Try.failure(DebugError("Invalid URL")) return httpClient.fetchWithDecoder(HttpRequest(url)) { val result = it.body diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index 89274cba49..ae34d513d8 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.launch import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetriever import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.data.ReadError @@ -174,7 +174,7 @@ class Bookshelf( coverFile.delete() return Try.failure( ImportError.DatabaseError( - MessageError("Could not insert book into database.") + DebugError("Could not insert book into database.") ) ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index 65d42b59d4..0d00451dda 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -20,11 +20,11 @@ import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.opds.images import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.file.FileSystemError @@ -161,14 +161,14 @@ class LocalPublicationRetriever( } if ( - sourceAsset is Asset.Resource && + sourceAsset is ResourceAsset && sourceAsset.mediaType.matches(MediaType.LCP_LICENSE_DOCUMENT) ) { if (lcpPublicationRetriever == null) { listener.onError( ImportError.PublicationError( PublicationError.UnsupportedContentProtection( - MessageError("LCP support is missing.") + DebugError("LCP support is missing.") ) ) ) @@ -272,7 +272,7 @@ class OpdsPublicationRetriever( val acquisitionUrl = links .filter { it.mediaType?.isPublication == true || it.mediaType == MediaType.LCP_LICENSE_DOCUMENT } .firstNotNullOfOrNull { it.url() as? AbsoluteUrl } - ?: return Try.failure(MessageError("No supported link to acquire publication.")) + ?: return Try.failure(DebugError("No supported link to acquire publication.")) return Try.success(acquisitionUrl) } @@ -349,7 +349,7 @@ class LcpPublicationRetriever( * Retrieves a publication protected with the given license. */ fun retrieve( - licenceAsset: Asset.Resource, + licenceAsset: ResourceAsset, licenceFile: File, coverUrl: AbsoluteUrl? ) { diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt index fa6ad9265a..7ef72edd52 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt @@ -78,7 +78,7 @@ sealed class ReadUserError( HttpConnectivity(error) is HttpError.Unreachable -> HttpConnectivity(error) - is HttpError.Response -> + is HttpError.ErrorResponse -> when (error.status) { HttpStatus.Forbidden -> HttpForbidden(error) HttpStatus.NotFound -> HttpNotFound(error) diff --git a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt index 762b5c9839..7ecab5c6a3 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt @@ -9,8 +9,8 @@ package org.readium.r2.testapp.drm import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel import java.util.* +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.MessageError import org.readium.r2.shared.util.Try abstract class DrmManagementViewModel : ViewModel() { @@ -36,10 +36,10 @@ abstract class DrmManagementViewModel : ViewModel() { open val canRenewLoan: Boolean = false open suspend fun renewLoan(fragment: Fragment): Try = - Try.failure(MessageError("Renewing a loan is not supported")) + Try.failure(DebugError("Renewing a loan is not supported")) open val canReturnPublication: Boolean = false open suspend fun returnPublication(): Try = - Try.failure(MessageError("Returning a publication is not supported")) + Try.failure(DebugError("Returning a publication is not supported")) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt index 74989df362..9fe369d733 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt @@ -22,7 +22,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.allAreHtml import org.readium.r2.shared.publication.services.isRestricted import org.readium.r2.shared.publication.services.protectionError -import org.readium.r2.shared.util.MessageError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.getOrElse import org.readium.r2.testapp.Readium @@ -100,7 +100,7 @@ class ReaderRepository( return Try.failure( OpeningError.RestrictedPublication( publication.protectionError - ?: MessageError("Publication is restricted.") + ?: DebugError("Publication is restricted.") ) ) } @@ -121,7 +121,7 @@ class ReaderRepository( Try.failure( OpeningError.PublicationError( PublicationError.UnsupportedPublication( - MessageError("No navigator supports this publication.") + DebugError("No navigator supports this publication.") ) ) ) @@ -145,7 +145,7 @@ class ReaderRepository( ) ?: return Try.failure( OpeningError.PublicationError( PublicationError.UnsupportedPublication( - MessageError("Cannot create audio navigator factory.") + DebugError("Cannot create audio navigator factory.") ) ) ) From cb567c998719d3260c319f42756b0c4637156a26 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 6 Dec 2023 10:32:11 +0100 Subject: [PATCH 49/86] Minor changes --- .../readium/r2/lcp/LcpContentProtection.kt | 30 +++++------ .../readium/r2/lcp/LcpPublicationRetriever.kt | 2 +- .../readium/r2/lcp/service/NetworkService.kt | 2 +- .../r2/shared/publication/Publication.kt | 15 +----- .../AdeptFallbackContentProtection.kt | 4 +- .../protection/ContentProtection.kt | 15 +++--- .../LcpFallbackContentProtection.kt | 4 +- .../publication/services/PositionsService.kt | 8 +-- .../r2/shared/util/asset/AssetRetriever.kt | 35 ++++++------- .../readium/r2/shared/util/data/Decoding.kt | 12 ----- .../shared/util/downloads/DownloadManager.kt | 35 ++++++------- .../android/AndroidDownloadManager.kt | 31 ++++++----- .../foreground/ForegroundDownloadManager.kt | 2 +- .../shared/util/mediatype/MediaTypeSniffer.kt | 13 ++++- .../readium/r2/streamer/PublicationFactory.kt | 51 ++++++++++--------- .../parser/readium/ReadiumWebPubParser.kt | 20 +++++--- .../readium/r2/testapp/domain/ImportError.kt | 2 +- .../r2/testapp/domain/ImportUserError.kt | 2 +- .../r2/testapp/domain/PublicationError.kt | 16 +++--- .../r2/testapp/domain/PublicationRetriever.kt | 2 +- 20 files changed, 148 insertions(+), 153 deletions(-) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index 84e975b637..917d0e5e7c 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -43,7 +43,7 @@ internal class LcpContentProtection( asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { return when (asset) { is ContainerAsset -> openPublication(asset, credentials, allowUserInteraction) is ResourceAsset -> openLicense(asset, credentials, allowUserInteraction) @@ -54,7 +54,7 @@ internal class LcpContentProtection( asset: ContainerAsset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { val license = retrieveLicense(asset, credentials, allowUserInteraction) return createResultAsset(asset, license) } @@ -74,7 +74,7 @@ internal class LcpContentProtection( private fun createResultAsset( asset: ContainerAsset, license: Try - ): Try { + ): Try { val serviceFactory = LcpContentProtectionService .createFactory(license.getOrNull(), license.failureOrNull()) @@ -104,7 +104,7 @@ internal class LcpContentProtection( licenseAsset: ResourceAsset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction) val licenseDoc = license.getOrNull()?.license @@ -114,7 +114,7 @@ internal class LcpContentProtection( LicenseDocument(it) } catch (e: Exception) { return Try.failure( - ContentProtection.Error.Reading( + ContentProtection.OpenError.Reading( ReadError.Decoding( DebugError( "Failed to read the LCP license document", @@ -127,14 +127,14 @@ internal class LcpContentProtection( } .getOrElse { return Try.failure( - ContentProtection.Error.Reading(it) + ContentProtection.OpenError.Reading(it) ) } val link = licenseDoc.publicationLink val url = (link.url() as? AbsoluteUrl) ?: return Try.failure( - ContentProtection.Error.Reading( + ContentProtection.OpenError.Reading( ReadError.Decoding( DebugError( "The LCP license document does not contain a valid link to the publication" @@ -159,7 +159,7 @@ internal class LcpContentProtection( Try.success((it)) } else { Try.failure( - ContentProtection.Error.AssetNotSupported( + ContentProtection.OpenError.AssetNotSupported( DebugError( "LCP license points to an unsupported publication." ) @@ -172,13 +172,13 @@ internal class LcpContentProtection( return asset.flatMap { createResultAsset(it, license) } } - private fun AssetRetriever.Error.wrap(): ContentProtection.Error = + private fun AssetRetriever.RetrieveError.wrap(): ContentProtection.OpenError = when (this) { - is AssetRetriever.Error.FormatNotSupported -> - ContentProtection.Error.AssetNotSupported(this) - is AssetRetriever.Error.Reading -> - ContentProtection.Error.Reading(cause) - is AssetRetriever.Error.SchemeNotSupported -> - ContentProtection.Error.AssetNotSupported(this) + is AssetRetriever.RetrieveError.FormatNotSupported -> + ContentProtection.OpenError.AssetNotSupported(this) + is AssetRetriever.RetrieveError.Reading -> + ContentProtection.OpenError.Reading(cause) + is AssetRetriever.RetrieveError.SchemeNotSupported -> + ContentProtection.OpenError.AssetNotSupported(this) } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt index 1b8e2b7a44..6e66e45989 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt @@ -259,7 +259,7 @@ public class LcpPublicationRetriever( override fun onDownloadFailed( requestId: DownloadManager.RequestId, - error: DownloadManager.Error + error: DownloadManager.DownloadError ) { val lcpRequestId = RequestId(requestId.value) val listenersForId = checkNotNull(listeners[lcpRequestId]) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt index d5915a0145..eff6f270f0 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt @@ -148,7 +148,7 @@ internal class NetworkService( ).getOrElse { when (it) { is MediaTypeSnifferError.NotRecognized -> - MediaType.BINARY + null is MediaTypeSnifferError.Reading -> throw ReadException(it.cause) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index e5e7124c9c..39a143c47a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -25,15 +25,12 @@ import org.readium.r2.shared.publication.services.DefaultLocatorService import org.readium.r2.shared.publication.services.LocatorService import org.readium.r2.shared.publication.services.PositionsService import org.readium.r2.shared.publication.services.ResourceCoverService -import org.readium.r2.shared.publication.services.WebPositionsService import org.readium.r2.shared.publication.services.content.ContentService import org.readium.r2.shared.publication.services.search.SearchService import org.readium.r2.shared.util.Closeable import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.EmptyContainer -import org.readium.r2.shared.util.http.DefaultHttpClient -import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.resource.Resource internal typealias ServiceFactory = (Publication.Service.Context) -> Publication.Service? @@ -57,13 +54,11 @@ public typealias PublicationId = String * The default implementation returns Resource.Exception.NotFound for all HREFs. * @param servicesBuilder Holds the list of service factories used to create the instances of * Publication.Service attached to this Publication. - * @param httpClient An [HttpClient] to access remote services. */ public class Publication( public val manifest: Manifest, private val container: Container = EmptyContainer(), private val servicesBuilder: ServicesBuilder = ServicesBuilder(), - httpClient: HttpClient = DefaultHttpClient(), @Deprecated( "Migrate to the new Settings API (see migration guide)", level = DeprecationLevel.ERROR @@ -80,8 +75,7 @@ public class Publication( init { services.services = servicesBuilder.build( - context = Service.Context(manifest, container, services), - httpClient = httpClient + context = Service.Context(manifest, container, services) ) } @@ -384,7 +378,7 @@ public class Publication( ) /** Builds the actual list of publication services to use in a Publication. */ - public fun build(context: Service.Context, httpClient: HttpClient? = null): List { + public fun build(context: Service.Context): List { val serviceFactories = buildMap { putAll(this@ServicesBuilder.serviceFactories) @@ -396,11 +390,6 @@ public class Publication( put(LocatorService::class.java.simpleName, factory) } - if (httpClient != null && !containsKey(PositionsService::class.java.simpleName)) { - val factory = WebPositionsService.createFactory(httpClient) - put(PositionsService::class.java.simpleName, factory) - } - if (!containsKey(CoverService::class.java.simpleName)) { val factory = ResourceCoverService.createFactory() put(CoverService::class.java.simpleName, factory) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt index 979800c623..c8d54b246c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt @@ -41,10 +41,10 @@ public class AdeptFallbackContentProtection : ContentProtection { asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { if (asset !is ContainerAsset) { return Try.failure( - ContentProtection.Error.AssetNotSupported( + ContentProtection.OpenError.AssetNotSupported( DebugError("A container asset was expected.") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index 4f259de723..1847e83eea 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -17,6 +17,7 @@ import kotlin.Unit import org.readium.r2.shared.publication.LocalizedString import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.ContentProtectionService +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError @@ -32,18 +33,18 @@ import org.readium.r2.shared.util.resource.Resource */ public interface ContentProtection { - public sealed class Error( + public sealed class OpenError( override val message: String, - override val cause: org.readium.r2.shared.util.Error? - ) : org.readium.r2.shared.util.Error { + override val cause: Error? + ) : Error { public class Reading( override val cause: ReadError - ) : Error("An error occurred while trying to read asset.", cause) + ) : OpenError("An error occurred while trying to read asset.", cause) public class AssetNotSupported( - override val cause: org.readium.r2.shared.util.Error? - ) : Error("Asset is not supported.", cause) + override val cause: Error? + ) : OpenError("Asset is not supported.", cause) } public val scheme: Scheme @@ -65,7 +66,7 @@ public interface ContentProtection { asset: org.readium.r2.shared.util.asset.Asset, credentials: String?, allowUserInteraction: Boolean - ): Try + ): Try /** * Holds the result of opening an [Asset] with a [ContentProtection]. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index 9adef5671e..aba8afc318 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -51,10 +51,10 @@ public class LcpFallbackContentProtection : ContentProtection { asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { if (asset !is ContainerAsset) { return Try.failure( - ContentProtection.Error.AssetNotSupported( + ContentProtection.OpenError.AssetNotSupported( DebugError("A container asset was expected.") ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt index 69c074f31d..ee61f6e693 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt @@ -11,6 +11,7 @@ package org.readium.r2.shared.publication.services import kotlinx.coroutines.runBlocking import org.json.JSONObject +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.extensions.mapNotNull import org.readium.r2.shared.extensions.toJsonOrNull import org.readium.r2.shared.publication.Link @@ -119,7 +120,8 @@ public class PerResourcePositionsService( } } -internal class WebPositionsService( +@InternalReadiumApi +public class WebPositionsService( private val manifest: Manifest, private val httpClient: HttpClient ) : PositionsService { @@ -162,9 +164,9 @@ internal class WebPositionsService( .orEmpty() } - companion object { + public companion object { - fun createFactory(httpClient: HttpClient): (Publication.Service.Context) -> WebPositionsService = { + public fun createFactory(httpClient: HttpClient): (Publication.Service.Context) -> WebPositionsService = { WebPositionsService(it.manifest, httpClient) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index db3f6bf379..7e7aeb2d60 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -9,6 +9,7 @@ package org.readium.r2.shared.util.asset import java.io.File import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.archive.ArchiveFactory @@ -32,21 +33,21 @@ public class AssetRetriever( formatRegistry: FormatRegistry ) { - public sealed class Error( + public sealed class RetrieveError( override val message: String, - override val cause: org.readium.r2.shared.util.Error? - ) : org.readium.r2.shared.util.Error { + override val cause: Error? + ) : Error { public class SchemeNotSupported( public val scheme: Url.Scheme, - cause: org.readium.r2.shared.util.Error? = null - ) : Error("Url scheme $scheme is not supported.", cause) + cause: Error? = null + ) : RetrieveError("Url scheme $scheme is not supported.", cause) - public class FormatNotSupported(cause: org.readium.r2.shared.util.Error) : - Error("Asset format is not supported.", cause) + public class FormatNotSupported(cause: Error) : + RetrieveError("Asset format is not supported.", cause) public class Reading(override val cause: org.readium.r2.shared.util.data.ReadError) : - Error("An error occurred when trying to read asset.", cause) + RetrieveError("An error occurred when trying to read asset.", cause) } private val archiveFactory: ArchiveFactory = @@ -58,7 +59,7 @@ public class AssetRetriever( public suspend fun retrieve( url: AbsoluteUrl, mediaType: MediaType - ): Try { + ): Try { val resource = retrieveResource(url, mediaType) .getOrElse { return Try.failure(it) } @@ -66,7 +67,7 @@ public class AssetRetriever( .getOrElse { return when (it) { is ArchiveFactory.Error.Reading -> - Try.failure(Error.Reading(it.cause)) + Try.failure(RetrieveError.Reading(it.cause)) is ArchiveFactory.Error.FormatNotSupported -> Try.success(ResourceAsset(mediaType, resource)) } @@ -78,12 +79,12 @@ public class AssetRetriever( private suspend fun retrieveResource( url: AbsoluteUrl, mediaType: MediaType - ): Try { + ): Try { return resourceFactory.create(url, mediaType) .mapFailure { error -> when (error) { is ResourceFactory.Error.SchemeNotSupported -> - Error.SchemeNotSupported(error.scheme, error) + RetrieveError.SchemeNotSupported(error.scheme, error) } } } @@ -93,19 +94,19 @@ public class AssetRetriever( /** * Retrieves an asset from an unknown local file. */ - public suspend fun retrieve(file: File): Try = + public suspend fun retrieve(file: File): Try = retrieve(file.toUrl()) /** * Retrieves an asset from an unknown [AbsoluteUrl]. */ - public suspend fun retrieve(url: AbsoluteUrl): Try { + public suspend fun retrieve(url: AbsoluteUrl): Try { val resource = resourceFactory.create(url) .getOrElse { return Try.failure( when (it) { is ResourceFactory.Error.SchemeNotSupported -> - Error.SchemeNotSupported(it.scheme) + RetrieveError.SchemeNotSupported(it.scheme) } ) } @@ -113,7 +114,7 @@ public class AssetRetriever( val mediaType = mediaTypeRetriever.retrieve(resource) .getOrElse { return Try.failure( - Error.FormatNotSupported( + RetrieveError.FormatNotSupported( DebugError("Cannot determine asset media type.") ) ) @@ -123,7 +124,7 @@ public class AssetRetriever( .getOrElse { when (it) { is ArchiveFactory.Error.Reading -> - return Try.failure(Error.Reading(it.cause)) + return Try.failure(RetrieveError.Reading(it.cause)) is ArchiveFactory.Error.FormatNotSupported -> return Try.success(ResourceAsset(mediaType, resource)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index 20e2094aa9..b1fbf83bee 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -19,7 +19,6 @@ import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.flatMap -import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.xml.ElementNode import org.readium.r2.shared.util.xml.XmlParser @@ -131,14 +130,3 @@ public suspend fun Readable.readAsBitmap(): Try = ) ) } - -/** - * Returns whether the content is a JSON object containing all of the given root keys. - */ -public suspend fun Readable.containsJsonKeys( - vararg keys: String -): Try { - val json = readAsJson() - .getOrElse { return Try.failure(it) } - return Try.success(json.keys().asSequence().toSet().containsAll(keys.toList())) -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt index 4fd85ae7c6..822680b90e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt @@ -8,6 +8,7 @@ package org.readium.r2.shared.util.downloads import java.io.File import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager import org.readium.r2.shared.util.downloads.foreground.ForegroundDownloadManager import org.readium.r2.shared.util.mediatype.MediaType @@ -36,38 +37,34 @@ public interface DownloadManager { @JvmInline public value class RequestId(public val value: String) - public sealed class Error( + public sealed class DownloadError( override val message: String, - override val cause: org.readium.r2.shared.util.Error? = null - ) : org.readium.r2.shared.util.Error { + override val cause: Error? = null + ) : Error { public class HttpError( cause: org.readium.r2.shared.util.http.HttpError - ) : Error(cause.message, cause) + ) : DownloadError(cause.message, cause) public class DeviceNotFound( - cause: org.readium.r2.shared.util.Error? = null - ) : Error("The storage device is missing.", cause) + cause: Error? = null + ) : DownloadError("The storage device is missing.", cause) public class CannotResume( - cause: org.readium.r2.shared.util.Error? = null - ) : Error("Download couldn't be resumed.", cause) + cause: Error? = null + ) : DownloadError("Download couldn't be resumed.", cause) public class InsufficientSpace( - cause: org.readium.r2.shared.util.Error? = null - ) : Error("There is not enough space to complete the download.", cause) + cause: Error? = null + ) : DownloadError("There is not enough space to complete the download.", cause) public class FileSystemError( - cause: org.readium.r2.shared.util.Error? = null - ) : Error("IO error on the local device.", cause) - - public class TooManyRedirects( - cause: org.readium.r2.shared.util.Error? = null - ) : Error("Too many redirects.", cause) + cause: Error? = null + ) : DownloadError("IO error on the local device.", cause) public class Unknown( - cause: org.readium.r2.shared.util.Error? = null - ) : Error("An unknown error occurred.", cause) + cause: Error? = null + ) : DownloadError("An unknown error occurred.", cause) } public interface Listener { @@ -85,7 +82,7 @@ public interface DownloadManager { /** * The download with ID [requestId] failed due to [error]. */ - public fun onDownloadFailed(requestId: RequestId, error: Error) + public fun onDownloadFailed(requestId: RequestId, error: DownloadError) /** * The download with ID [requestId] has been cancelled. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index a5074d94a1..fecbc5e5fa 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -27,7 +27,6 @@ import org.readium.r2.shared.extensions.tryOr import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.downloads.DownloadManager -import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.shared.util.mediatype.FormatRegistry @@ -272,14 +271,14 @@ public class AndroidDownloadManager internal constructor( } } - private suspend fun prepareResult(destFile: File, mediaTypeHint: MediaType?): Try = + private suspend fun prepareResult(destFile: File, mediaTypeHint: MediaType?): Try = withContext(Dispatchers.IO) { val mediaType = mediaTypeRetriever.retrieve( destFile, MediaTypeHints(mediaType = mediaTypeHint) - ).getOrElse { MediaType.BINARY } + ).getOrNull() - val extension = formatRegistry.fileExtension(mediaType) + val extension = mediaType?.let { formatRegistry.fileExtension(it) } ?: destFile.extension.takeUnless { it.isEmpty() } val newDest = File(destFile.parent, generateFileName(extension)) @@ -293,37 +292,37 @@ public class AndroidDownloadManager internal constructor( Try.success(download) } else { Try.failure( - DownloadManager.Error.FileSystemError( + DownloadManager.DownloadError.FileSystemError( DebugError("Failed to rename the downloaded file.") ) ) } } - private fun mapErrorCode(code: Int): DownloadManager.Error = + private fun mapErrorCode(code: Int): DownloadManager.DownloadError = when (code) { in 400 until 1000 -> - DownloadManager.Error.HttpError(httpErrorForCode(code)) + DownloadManager.DownloadError.HttpError(httpErrorForCode(code)) SystemDownloadManager.ERROR_UNHANDLED_HTTP_CODE -> - DownloadManager.Error.HttpError(httpErrorForCode(code)) + DownloadManager.DownloadError.HttpError(httpErrorForCode(code)) SystemDownloadManager.ERROR_HTTP_DATA_ERROR -> - DownloadManager.Error.HttpError(HttpError.MalformedResponse(null)) + DownloadManager.DownloadError.HttpError(HttpError.MalformedResponse(null)) SystemDownloadManager.ERROR_TOO_MANY_REDIRECTS -> - DownloadManager.Error.HttpError( + DownloadManager.DownloadError.HttpError( HttpError.Redirection(DebugError("Too many redirects.")) ) SystemDownloadManager.ERROR_CANNOT_RESUME -> - DownloadManager.Error.CannotResume() + DownloadManager.DownloadError.CannotResume() SystemDownloadManager.ERROR_DEVICE_NOT_FOUND -> - DownloadManager.Error.DeviceNotFound() + DownloadManager.DownloadError.DeviceNotFound() SystemDownloadManager.ERROR_FILE_ERROR -> - DownloadManager.Error.FileSystemError() + DownloadManager.DownloadError.FileSystemError() SystemDownloadManager.ERROR_INSUFFICIENT_SPACE -> - DownloadManager.Error.InsufficientSpace() + DownloadManager.DownloadError.InsufficientSpace() SystemDownloadManager.ERROR_UNKNOWN -> - DownloadManager.Error.Unknown() + DownloadManager.DownloadError.Unknown() else -> - DownloadManager.Error.Unknown() + DownloadManager.DownloadError.Unknown() } private fun httpErrorForCode(code: Int): HttpError = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt index 9b2cda3d82..c3670ddd2f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt @@ -87,7 +87,7 @@ public class ForegroundDownloadManager( } .onFailure { error -> forEachListener(id) { - onDownloadFailed(id, DownloadManager.Error.HttpError(error)) + onDownloadFailed(id, DownloadManager.DownloadError.HttpError(error)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index a824a6cde7..b4f1cf5f6e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -28,7 +28,6 @@ import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.data.asInputStream import org.readium.r2.shared.util.data.borrow -import org.readium.r2.shared.util.data.containsJsonKeys import org.readium.r2.shared.util.data.readAsJson import org.readium.r2.shared.util.data.readAsRwpm import org.readium.r2.shared.util.data.readAsString @@ -939,3 +938,15 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { private suspend fun Readable.canReadWholeBlob() = length().getOrDefault(0) < 5 * 1000 * 1000 + +/** + * Returns whether the content is a JSON object containing all of the given root keys. + */ +@Suppress("SameParameterValue") +private suspend fun Readable.containsJsonKeys( + vararg keys: String +): Try { + val json = readAsJson() + .getOrElse { return Try.failure(it) } + return Try.success(json.keys().asSequence().toSet().containsAll(keys.toList())) +} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 02543e513f..885e7b923d 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -13,6 +13,7 @@ import org.readium.r2.shared.publication.protection.AdeptFallbackContentProtecti import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.publication.protection.LcpFallbackContentProtection import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.getOrElse @@ -55,27 +56,27 @@ public class PublicationFactory( ignoreDefaultParsers: Boolean = false, contentProtections: List, formatRegistry: FormatRegistry, - httpClient: HttpClient, + private val httpClient: HttpClient, pdfFactory: PdfDocumentFactory<*>?, private val mediaTypeRetriever: MediaTypeRetriever, private val onCreatePublication: Publication.Builder.() -> Unit = {} ) { - public sealed class Error( + public sealed class OpenError( override val message: String, - override val cause: org.readium.r2.shared.util.Error? - ) : org.readium.r2.shared.util.Error { + override val cause: Error? + ) : Error { public class Reading( override val cause: org.readium.r2.shared.util.data.ReadError - ) : Error("An error occurred while trying to read asset.", cause) + ) : OpenError("An error occurred while trying to read asset.", cause) public class FormatNotSupported( - override val cause: org.readium.r2.shared.util.Error? - ) : Error("Asset is not supported.", cause) + override val cause: Error? + ) : OpenError("Asset is not supported.", cause) public class ContentProtectionNotSupported( - override val cause: org.readium.r2.shared.util.Error? = null - ) : Error("No ContentProtection available to open asset.", cause) + override val cause: Error? = null + ) : OpenError("No ContentProtection available to open asset.", cause) } public companion object { @@ -123,7 +124,7 @@ public class PublicationFactory( listOfNotNull( EpubParser(), pdfFactory?.let { PdfParser(context, it) }, - ReadiumWebPubParser(context, pdfFactory), + ReadiumWebPubParser(context, httpClient, pdfFactory), ImageParser(mediaTypeRetriever), AudioParser(mediaTypeRetriever) ) @@ -157,7 +158,7 @@ public class PublicationFactory( * It can be used to modify the manifest, the root container or the list of service * factories of the [Publication]. * @param warnings Logger used to broadcast non-fatal parsing warnings. - * @return A [Publication] or a [Error] in case of failure. + * @return A [Publication] or a [OpenError] in case of failure. */ public suspend fun open( asset: Asset, @@ -166,7 +167,7 @@ public class PublicationFactory( allowUserInteraction: Boolean, onCreatePublication: Publication.Builder.() -> Unit = {}, warnings: WarningLogger? = null - ): Try { + ): Try { val compositeOnCreatePublication: Publication.Builder.() -> Unit = { this@PublicationFactory.onCreatePublication(this) onCreatePublication(this) @@ -194,14 +195,14 @@ public class PublicationFactory( asset: Asset, onCreatePublication: Publication.Builder.() -> Unit, warnings: WarningLogger? - ): Try { + ): Try { val parserAsset = parserAssetFactory.createParserAsset(asset) .mapFailure { when (it) { is ParserAssetFactory.Error.ReadError -> - Error.Reading(it.cause) + OpenError.Reading(it.cause) is ParserAssetFactory.Error.UnsupportedAsset -> - Error.FormatNotSupported(it.cause) + OpenError.FormatNotSupported(it.cause) } } .getOrElse { return Try.failure(it) } @@ -215,19 +216,19 @@ public class PublicationFactory( allowUserInteraction: Boolean, onCreatePublication: Publication.Builder.() -> Unit, warnings: WarningLogger? - ): Try { + ): Try { val protectedAsset = contentProtections[contentProtectionScheme] ?.open(asset, credentials, allowUserInteraction) ?.mapFailure { when (it) { - is ContentProtection.Error.Reading -> - Error.Reading(it.cause) - is ContentProtection.Error.AssetNotSupported -> - Error.FormatNotSupported(it) + is ContentProtection.OpenError.Reading -> + OpenError.Reading(it.cause) + is ContentProtection.OpenError.AssetNotSupported -> + OpenError.FormatNotSupported(it) } } ?.getOrElse { return Try.failure(it) } - ?: return Try.failure(Error.ContentProtectionNotSupported()) + ?: return Try.failure(OpenError.ContentProtectionNotSupported()) val parserAsset = PublicationParser.Asset( protectedAsset.mediaType, @@ -246,7 +247,7 @@ public class PublicationFactory( publicationAsset: PublicationParser.Asset, onCreatePublication: Publication.Builder.() -> Unit = {}, warnings: WarningLogger? = null - ): Try { + ): Try { val builder = parse(publicationAsset, warnings) .getOrElse { return Try.failure(wrapParserException(it)) } @@ -272,11 +273,11 @@ public class PublicationFactory( return Try.failure(PublicationParser.Error.FormatNotSupported()) } - private fun wrapParserException(e: PublicationParser.Error): Error = + private fun wrapParserException(e: PublicationParser.Error): OpenError = when (e) { is PublicationParser.Error.FormatNotSupported -> - Error.FormatNotSupported(DebugError("Cannot find a parser for this asset.")) + OpenError.FormatNotSupported(DebugError("Cannot find a parser for this asset.")) is PublicationParser.Error.Reading -> - Error.Reading(e.cause) + OpenError.Reading(e.cause) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index b39eb7e523..fe8ed6e63b 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -10,6 +10,7 @@ import android.content.Context import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.InMemoryCacheService import org.readium.r2.shared.publication.services.PerResourcePositionsService +import org.readium.r2.shared.publication.services.WebPositionsService import org.readium.r2.shared.publication.services.cacheServiceFactory import org.readium.r2.shared.publication.services.locatorServiceFactory import org.readium.r2.shared.publication.services.positionsServiceFactory @@ -20,6 +21,7 @@ import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.readAsRwpm import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.pdf.PdfDocumentFactory @@ -31,6 +33,7 @@ import org.readium.r2.streamer.parser.audio.AudioLocatorService */ public class ReadiumWebPubParser( private val context: Context? = null, + private val httpClient: HttpClient, private val pdfFactory: PdfDocumentFactory<*>? ) : PublicationParser { @@ -89,17 +92,20 @@ public class ReadiumWebPubParser( val servicesBuilder = Publication.ServicesBuilder().apply { cacheServiceFactory = InMemoryCacheService.createFactory(context) - when (asset.mediaType) { + positionsServiceFactory = when (asset.mediaType) { MediaType.LCP_PROTECTED_PDF -> - positionsServiceFactory = pdfFactory?.let { LcpdfPositionsService.create(it) } - + pdfFactory?.let { LcpdfPositionsService.create(it) } MediaType.DIVINA -> - positionsServiceFactory = PerResourcePositionsService.createFactory( - MediaType("image/*")!! - ) + PerResourcePositionsService.createFactory(MediaType("image/*")!!) + else -> + WebPositionsService.createFactory(httpClient) + } + locatorServiceFactory = when (asset.mediaType) { MediaType.READIUM_AUDIOBOOK, MediaType.LCP_PROTECTED_AUDIOBOOK -> - locatorServiceFactory = AudioLocatorService.createFactory() + AudioLocatorService.createFactory() + else -> + null } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt index d8fc24cc00..142079cd21 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt @@ -26,7 +26,7 @@ sealed class ImportError( ) : ImportError(cause) class DownloadFailed( - override val cause: DownloadManager.Error + override val cause: DownloadManager.DownloadError ) : ImportError(cause) class OpdsError(override val cause: Error) : diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportUserError.kt index 0926e9183e..0780b0c32e 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportUserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportUserError.kt @@ -32,7 +32,7 @@ sealed class ImportUserError( ) : ImportUserError(cause) class DownloadFailed( - val error: DownloadManager.Error + val error: DownloadManager.DownloadError ) : ImportUserError(R.string.import_publication_download_failed) class OpdsError( diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index 1555e98842..0560aee340 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -38,13 +38,13 @@ sealed class PublicationError( companion object { - operator fun invoke(error: AssetRetriever.Error): PublicationError = + operator fun invoke(error: AssetRetriever.RetrieveError): PublicationError = when (error) { - is AssetRetriever.Error.Reading -> + is AssetRetriever.RetrieveError.Reading -> ReadError(error.cause) - is AssetRetriever.Error.FormatNotSupported -> + is AssetRetriever.RetrieveError.FormatNotSupported -> UnsupportedArchiveFormat(error) - is AssetRetriever.Error.SchemeNotSupported -> + is AssetRetriever.RetrieveError.SchemeNotSupported -> UnsupportedScheme(error) } @@ -56,13 +56,13 @@ sealed class PublicationError( UnsupportedContentProtection(error) } - operator fun invoke(error: PublicationFactory.Error): PublicationError = + operator fun invoke(error: PublicationFactory.OpenError): PublicationError = when (error) { - is PublicationFactory.Error.Reading -> + is PublicationFactory.OpenError.Reading -> ReadError(error.cause) - is PublicationFactory.Error.FormatNotSupported -> + is PublicationFactory.OpenError.FormatNotSupported -> UnsupportedPublication(error) - is PublicationFactory.Error.ContentProtectionNotSupported -> + is PublicationFactory.OpenError.ContentProtectionNotSupported -> UnsupportedContentProtection(error) } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index 0d00451dda..3579ac2f30 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -305,7 +305,7 @@ class OpdsPublicationRetriever( override fun onDownloadFailed( requestId: DownloadManager.RequestId, - error: DownloadManager.Error + error: DownloadManager.DownloadError ) { coroutineScope.launch { downloadRepository.remove(requestId.value) From 16eca619f8ddefb582106e73ecd409b0b2a9b86d Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 6 Dec 2023 13:15:01 +0100 Subject: [PATCH 50/86] Fix SearchService errors --- .../services/search/SearchService.kt | 41 ++++--------------- .../services/search/StringSearchService.kt | 31 +++++++------- .../r2/testapp/reader/ReaderViewModel.kt | 9 ++-- .../r2/testapp/search/SearchUserError.kt | 34 +++------------ 4 files changed, 35 insertions(+), 80 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt index d97f5e0255..a2842de48e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.data.ReadError @ExperimentalReadiumApi public typealias SearchTry = Try @@ -29,43 +29,19 @@ public sealed class SearchError( override val cause: Error? = null ) : Error { - /** - * The publication is not searchable. - */ - public object PublicationNotSearchable : - SearchError("This publication is not searchable.") - - /** - * The provided search query cannot be handled by the service. - */ - public class BadQuery(cause: Error) : - SearchError("The provided search query cannot be handled by the service.", cause) - /** * An error occurred while accessing one of the publication's resources. */ - public class ResourceError(cause: Error) : + public class Reading(override val cause: ReadError) : SearchError( "An error occurred while accessing one of the publication's resources.", cause ) /** - * An error occurred while performing an HTTP request. - */ - public class NetworkError(cause: HttpError) : - SearchError("An error occurred while performing an HTTP request.", cause) - - /** - * The search was cancelled by the caller. - * - * For example, when a coroutine or a network request is cancelled. - */ - public object Cancelled : - SearchError("The search was cancelled.") - - /** For any other custom service error. */ - public class Other(cause: Error) : + * An error occurring in the search engine. + * */ + public class Engine(cause: Error) : SearchError("An error occurred while searching.", cause) } @@ -119,7 +95,7 @@ public interface SearchService : Publication.Service { * * If an option is nil when calling search(), its value is assumed to be the default one. */ - public suspend fun search(query: String, options: Options? = null): SearchTry + public suspend fun search(query: String, options: Options? = null): SearchIterator } /** @@ -141,11 +117,12 @@ public val Publication.searchOptions: SearchService.Options get() = * * If an option is nil when calling [search], its value is assumed to be the default one for the * search service. + * + * Returns null if the publication is not searchable. */ @ExperimentalReadiumApi -public suspend fun Publication.search(query: String, options: SearchService.Options? = null): SearchTry = +public suspend fun Publication.search(query: String, options: SearchService.Options? = null): SearchIterator? = findService(SearchService::class)?.search(query, options) - ?: Try.failure(SearchError.PublicationNotSearchable) /** Factory to build a [SearchService] */ @ExperimentalReadiumApi diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt index be84a77258..ecc8dfac01 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt @@ -14,6 +14,7 @@ import android.os.Build import androidx.annotation.RequiresApi import java.text.StringCharacterIterator import java.util.* +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.ExperimentalReadiumApi @@ -76,20 +77,14 @@ public class StringSearchService( override val options: Options = searchAlgorithm.options .copy(language = locale.toLanguageTag()) - override suspend fun search(query: String, options: Options?): SearchTry = - try { - Try.success( - Iterator( - manifest = manifest, - container = container, - query = query, - options = options ?: Options(), - locale = options?.language?.let { Locale.forLanguageTag(it) } ?: locale - ) - ) - } catch (e: Exception) { - Try.failure(SearchError.Other(ThrowableError(e))) - } + override suspend fun search(query: String, options: Options?): SearchIterator = + Iterator( + manifest = manifest, + container = container, + query = query, + options = options ?: Options(), + locale = options?.language?.let { Locale.forLanguageTag(it) } ?: locale + ) private inner class Iterator( val manifest: Manifest, @@ -120,7 +115,7 @@ public class StringSearchService( val text = container[link.url()] ?.let { extractorFactory.createExtractor(it, mediaType)?.extractText(it) } - ?.getOrElse { return Try.failure(SearchError.ResourceError(it)) } + ?.getOrElse { return Try.failure(SearchError.Reading(it)) } if (text == null) { Timber.w("Cannot extract text from resource: ${link.href}") @@ -137,8 +132,12 @@ public class StringSearchService( } return Try.success(LocatorCollection(locators = locators)) + } catch ( + e: CancellationException + ) { + throw e } catch (e: Exception) { - return Try.failure(SearchError.Other(ThrowableError(e))) + return Try.failure(SearchError.Engine(ThrowableError(e))) } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt index 46ade8f460..840347b5c7 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt @@ -221,11 +221,12 @@ class ReaderViewModel( lastSearchQuery = query _searchLocators.value = emptyList() searchIterator = publication.search(query) - .onFailure { - Timber.e(it.toDebugDescription()) - activityChannel.send(ActivityCommand.ToastError(SearchUserError(it))) + ?: run { + activityChannel.send( + ActivityCommand.ToastError(SearchUserError.PublicationNotSearchable) + ) + null } - .getOrNull() pagingSourceFactory.invalidate() searchChannel.send(SearchCommand.StartNewSearch) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt index bf982b7577..da80e503e0 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt @@ -22,19 +22,10 @@ sealed class SearchUserError( object PublicationNotSearchable : SearchUserError(R.string.search_error_not_searchable) - class BadQuery(val error: Error) : - SearchUserError(R.string.search_error_not_searchable) - - class ResourceError(val error: Error) : + class Reading(val error: Error) : SearchUserError(R.string.search_error_other) - class NetworkError(val error: Error) : - SearchUserError(R.string.search_error_other) - - object Cancelled : - SearchUserError(R.string.search_error_cancelled) - - class Other(val error: Error) : + class Engine(val error: Error) : SearchUserError(R.string.search_error_other) companion object { @@ -42,23 +33,10 @@ sealed class SearchUserError( @OptIn(ExperimentalReadiumApi::class) operator fun invoke(error: SearchError): SearchUserError = when (error) { - is SearchError.BadQuery -> - BadQuery(error) - - SearchError.Cancelled -> - Cancelled - - is SearchError.NetworkError -> - NetworkError(error) - - is SearchError.Other -> - Other(error) - - SearchError.PublicationNotSearchable -> - PublicationNotSearchable - - is SearchError.ResourceError -> - ResourceError(error) + is SearchError.Reading -> + Reading(error) + is SearchError.Engine -> + Engine(error) } } } From 2adb5c8806bacf166dd24815a380f203b450706f Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 6 Dec 2023 13:39:38 +0100 Subject: [PATCH 51/86] Fix ExoMediaPlayer --- .../r2/navigator/media/ExoMediaPlayer.kt | 18 ++--- .../{audio => media}/PublicationDataSource.kt | 68 ++++++------------- .../publication/services/PositionsService.kt | 6 +- .../readium/r2/shared/util/http/HttpStatus.kt | 3 + 4 files changed, 30 insertions(+), 65 deletions(-) rename readium/navigator/src/main/java/org/readium/r2/navigator/{audio => media}/PublicationDataSource.kt (67%) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt index c8e32d841f..16cf2a0f50 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt @@ -33,10 +33,8 @@ import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator import com.google.android.exoplayer2.source.DefaultMediaSourceFactory import com.google.android.exoplayer2.ui.PlayerNotificationManager import com.google.android.exoplayer2.upstream.DataSource -import com.google.android.exoplayer2.upstream.HttpDataSource import com.google.android.exoplayer2.upstream.cache.Cache import com.google.android.exoplayer2.upstream.cache.CacheDataSource -import java.net.UnknownHostException import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart @@ -46,7 +44,6 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import org.readium.r2.navigator.ExperimentalAudiobook import org.readium.r2.navigator.R -import org.readium.r2.navigator.audio.PublicationDataSource import org.readium.r2.navigator.extensions.timeWithDuration import org.readium.r2.shared.extensions.asInstance import org.readium.r2.shared.publication.Link @@ -54,10 +51,8 @@ import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.PublicationId import org.readium.r2.shared.publication.indexOfFirstWithHref -import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.http.HttpError +import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.toUri import timber.log.Timber @@ -202,19 +197,14 @@ public class ExoMediaPlayer( } override fun onPlayerError(error: PlaybackException) { - var resourceError: ReadError? = error.asInstance() - if (resourceError == null && (error.cause as? HttpDataSource.HttpDataSourceException)?.cause is UnknownHostException) { - resourceError = ReadError.Access( - HttpError.Unreachable(ThrowableError(error.cause!!)) - ) - } + val readError = error.asInstance()?.error - if (resourceError != null) { + if (readError != null) { player.currentMediaItem?.mediaId ?.let { Url(it) } ?.let { href -> publication.linkWithHref(href) } ?.let { link -> - listener?.onResourceLoadFailed(link, resourceError) + listener?.onResourceLoadFailed(link, readError) } } else { Timber.e(error) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/PublicationDataSource.kt similarity index 67% rename from readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt rename to readium/navigator/src/main/java/org/readium/r2/navigator/media/PublicationDataSource.kt index 68f0d59b3a..582b38cca6 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/audio/PublicationDataSource.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/PublicationDataSource.kt @@ -7,7 +7,7 @@ // Everything in this file will be deprecated @file:Suppress("DEPRECATION") -package org.readium.r2.navigator.audio +package org.readium.r2.navigator.media import android.net.Uri import com.google.android.exoplayer2.C.LENGTH_UNSET @@ -16,7 +16,6 @@ import com.google.android.exoplayer2.upstream.BaseDataSource import com.google.android.exoplayer2.upstream.DataSource import com.google.android.exoplayer2.upstream.DataSpec import com.google.android.exoplayer2.upstream.TransferListener -import java.io.IOException import kotlinx.coroutines.runBlocking import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.data.ReadException @@ -25,18 +24,6 @@ import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.buffered import org.readium.r2.shared.util.toUrl -internal sealed class PublicationDataSourceException(message: String, cause: Throwable?) : IOException( - message, - cause -) { - class NotOpened(message: String) : PublicationDataSourceException(message, null) - class NotFound(message: String) : PublicationDataSourceException(message, null) - class ReadFailed(uri: Uri, offset: Int, readLength: Int, cause: Throwable) : PublicationDataSourceException( - "Failed to read $readLength bytes of URI $uri at offset $offset.", - cause - ) -} - /** * An ExoPlayer's [DataSource] which retrieves resources from a [Publication]. */ @@ -71,7 +58,7 @@ internal class PublicationDataSource(private val publication: Publication) : Bas ?.let { publication.get(it) } // Significantly improves performances, in particular with deflated ZIP entries. ?.buffered(resourceLength = cachedLengths[dataSpec.uri.toString()]) - ?: throw PublicationDataSourceException.NotFound( + ?: throw IllegalStateException( "Can't find a [Link] for URI: ${dataSpec.uri}. Make sure you only request resources declared in the manifest." ) @@ -111,42 +98,29 @@ internal class PublicationDataSource(private val publication: Publication) : Bas return 0 } - val openedResource = openedResource ?: throw PublicationDataSourceException.NotOpened( - "No opened resource to read from. Did you call open()?" - ) + val openedResource = openedResource + ?: throw IllegalStateException("No opened resource to read from. Did you call open()?") - try { - val data = runBlocking { - openedResource.resource - .read(range = openedResource.position until (openedResource.position + length)) - .mapFailure { ReadException(it) } - .getOrThrow() - } + val data = runBlocking { + openedResource.resource + .read(range = openedResource.position until (openedResource.position + length)) + .mapFailure { ReadException(it) } + .getOrThrow() + } - if (data.isEmpty()) { - return RESULT_END_OF_INPUT - } + if (data.isEmpty()) { + return RESULT_END_OF_INPUT + } - data.copyInto( - destination = target, - destinationOffset = offset, - startIndex = 0, - endIndex = data.size - ) + data.copyInto( + destination = target, + destinationOffset = offset, + startIndex = 0, + endIndex = data.size + ) - openedResource.position += data.count() - return data.count() - } catch (e: Exception) { - if (e is InterruptedException) { - return 0 - } - throw PublicationDataSourceException.ReadFailed( - uri = openedResource.uri, - offset = offset, - readLength = length, - cause = e - ) - } + openedResource.position += data.count() + return data.count() } override fun getUri(): Uri? = openedResource?.uri diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt index ee61f6e693..ea35fd6505 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/PositionsService.kt @@ -26,6 +26,7 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpRequest +import org.readium.r2.shared.util.http.fetchString import org.readium.r2.shared.util.mediatype.MediaType private val positionsMediaType = @@ -153,11 +154,8 @@ public class WebPositionsService( val positionsUrl = (positionsLink.url(base = selfLink?.url()) as? AbsoluteUrl) ?: return emptyList() - return httpClient.stream(HttpRequest(positionsUrl)) + return httpClient.fetchString(HttpRequest(positionsUrl)) .getOrNull() - ?.body - ?.readBytes() - ?.decodeToString() ?.toJsonOrNull() ?.optJSONArray("positions") ?.mapNotNull { Locator.fromJSON(it as? JSONObject) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt index e71790cf16..78824ab07c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpStatus.kt @@ -35,5 +35,8 @@ public value class HttpStatus( /** (405) Method not allowed. */ public val MethodNotAllowed: HttpStatus = HttpStatus(405) + + /** (500) Internal Server Error */ + public val InternalServerError: HttpStatus = HttpStatus(500) } } From 101017fee4d8043188bcaeaa53be844bc031b11f Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 6 Dec 2023 14:36:23 +0100 Subject: [PATCH 52/86] Refactor BufferingResource --- .../main/java/org/readium/r2/lcp/LcpError.kt | 2 +- .../AdeptFallbackContentProtection.kt | 2 +- .../shared/util/resource/BufferingResource.kt | 118 ++---------------- .../util/resource/BufferingResourceTest.kt | 2 +- 4 files changed, 10 insertions(+), 114 deletions(-) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt index 158f24624c..85d2ee611d 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpError.kt @@ -259,4 +259,4 @@ public typealias LCPError = LcpError replaceWith = ReplaceWith("getUserMessage(context)"), level = DeprecationLevel.ERROR ) -public val LcpError.errorDescription: String? get() = message +public val LcpError.errorDescription: String get() = message diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt index c8d54b246c..27c34b4ad6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt @@ -67,7 +67,7 @@ public class AdeptFallbackContentProtection : ContentProtection { return Try.success(false) } - asset.container.get(Url("META-INF/encryption.xml")!!) + asset.container[Url("META-INF/encryption.xml")!!] ?.readAsXml() ?.getOrElse { when (it) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt index edfb719528..74c6c139f5 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt @@ -6,11 +6,9 @@ package org.readium.r2.shared.util.resource -import org.readium.r2.shared.extensions.coerceIn -import org.readium.r2.shared.extensions.contains -import org.readium.r2.shared.extensions.requireLengthFitInt import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.ReadableBuffer /** * Wraps a [Resource] and buffers its content. @@ -32,116 +30,14 @@ import org.readium.r2.shared.util.data.ReadError public class BufferingResource( private val resource: Resource, resourceLength: Long? = null, - private val bufferSize: Long = DEFAULT_BUFFER_SIZE + private val bufferSize: Int = DEFAULT_BUFFER_SIZE ) : Resource by resource { - internal companion object { - internal const val DEFAULT_BUFFER_SIZE: Long = 8192 - } + private val buffer: ReadableBuffer = + ReadableBuffer(resource, resourceLength, bufferSize) - /** - * The buffer containing the current bytes read from the wrapped [Resource], with the range it - * covers. - */ - private var buffer: Pair? = null - - private lateinit var _cachedLength: Try - private suspend fun cachedLength(): Try { - if (!::_cachedLength.isInitialized) { - _cachedLength = resource.length() - } - return _cachedLength - } - - init { - if (resourceLength != null) { - _cachedLength = Try.success(resourceLength) - } - } - - override suspend fun read(range: LongRange?): Try { - val length = cachedLength().getOrNull() - // Reading the whole resource bypasses buffering to keep things simple. - if (range == null || length == null) { - return resource.read(range) - } - - val requestedRange = range - .coerceIn(0L until length) - .requireLengthFitInt() - if (requestedRange.isEmpty()) { - return Try.success(ByteArray(0)) - } - - // Round up the range to be read to the next `bufferSize`, because we will buffer the - // excess. - val readLast = (requestedRange.last + 1).ceilMultipleOf(bufferSize).coerceAtMost(length) - var readRange = requestedRange.first until readLast - - // Attempt to serve parts or all of the request using the buffer. - buffer?.let { pair -> - var (buffer, bufferedRange) = pair - - // Everything already buffered? - if (bufferedRange.contains(requestedRange)) { - val data = extractRange(requestedRange, buffer, start = bufferedRange.first) - return Try.success(data) - - // Beginning of requested data is buffered? - } else if (bufferedRange.contains(requestedRange.first)) { - readRange = (bufferedRange.last + 1)..readRange.last - - return resource.read(readRange).map { readData -> - buffer += readData - // Shift the current buffer to the tail of the read data. - saveBuffer(buffer, readRange) - - val bytes = extractRange(requestedRange, buffer, start = bufferedRange.first) - bytes - } - } - } - - // Fallback on reading the requested range from the original resource. - return resource.read(readRange).map { data -> - saveBuffer(data, readRange) - - val res = if (data.count() > requestedRange.count()) { - data.copyOfRange(0, requestedRange.count()) - } else { - data - } - - res - } - } - - /** - * Keeps the last chunk of the given data as the buffer for next reads. - * - * @param data Data read from the original resource. - * @param range Range of the read data in the resource. - */ - private fun saveBuffer(data: ByteArray, range: LongRange) { - val lastChunk = data.takeLast(bufferSize.toInt()).toByteArray() - val chunkRange = (range.last + 1 - lastChunk.count())..range.last - buffer = Pair(lastChunk, chunkRange) - } - - /** - * Reads a sub-range of the given [data] after shifting the given absolute (to the resource) - * ranges to be relative to [data]. - */ - private fun extractRange(requestedRange: LongRange, data: ByteArray, start: Long): ByteArray { - val first = requestedRange.first - start - val lastExclusive = first + requestedRange.count() - require(first >= 0) - require(lastExclusive <= data.count()) { "$lastExclusive > ${data.count()}" } - return data.copyOfRange(first.toInt(), lastExclusive.toInt()) - } - - private fun Long.ceilMultipleOf(divisor: Long) = - divisor * (this / divisor + if (this % divisor == 0L) 0 else 1) + override suspend fun read(range: LongRange?): Try = + buffer.read(range) } /** @@ -153,6 +49,6 @@ public class BufferingResource( */ public fun Resource.buffered( resourceLength: Long? = null, - size: Long = BufferingResource.DEFAULT_BUFFER_SIZE + size: Int = DEFAULT_BUFFER_SIZE ): BufferingResource = BufferingResource(resource = this, resourceLength = resourceLength, bufferSize = size) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt index d1fa49dc8b..265e044aab 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt @@ -126,7 +126,7 @@ class BufferingResourceTest { private val data = file.readBytes() private val resource = FileResource(file, MediaType.EPUB) - private fun sut(bufferSize: Long = 1024): BufferingResource = + private fun sut(bufferSize: Int = 1024): BufferingResource = BufferingResource(resource, bufferSize = bufferSize) private fun testRead(sut: BufferingResource, range: LongRange? = null) { From b25739ba3b11ebe161fd9da946fedf133928030f Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 6 Dec 2023 14:59:29 +0100 Subject: [PATCH 53/86] Cosmetic changes --- .../services/search/StringSearchService.kt | 12 +- .../readium/r2/shared/util/data/Buffering.kt | 155 ++++++++++++++++++ .../shared/util/resource/ZipContainerTest.kt | 8 +- 3 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt index ecc8dfac01..1db499bc6c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/StringSearchService.kt @@ -299,12 +299,12 @@ public class StringSearchService( val collator = Collator.getInstance(locale) as RuleBasedCollator if (!diacriticSensitive) { collator.strength = Collator.PRIMARY - if (caseSensitive) { - // FIXME: This doesn't seem to work despite the documentation indicating: - // > To ignore accents but take cases into account, set strength to primary and case level to on. - // > http://userguide.icu-project.org/collation/customization - collator.isCaseLevel = true - } + // if (caseSensitive) { + // FIXME: This doesn't seem to work despite the documentation indicating: + // > To ignore accents but take cases into account, set strength to primary and case level to on. + // > http://userguide.icu-project.org/collation/customization + // collator.isCaseLevel = true + // } } else if (!caseSensitive) { collator.strength = Collator.SECONDARY } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt new file mode 100644 index 0000000000..4393f67686 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import org.readium.r2.shared.extensions.coerceIn +import org.readium.r2.shared.extensions.contains +import org.readium.r2.shared.extensions.requireLengthFitInt +import org.readium.r2.shared.util.Try + +/** + * Wraps a [Readable] and buffers its content. + * + * @param source Underlying readable which will be buffered. + * @param contentLength The total length of the readable, when known. This can improve performance + * by avoiding requesting the length from the underlying resource. + * @param bufferSize Size of the buffer chunks to read. + */ +internal class ReadableBuffer internal constructor( + private val source: Readable, + contentLength: Long? = null, + private val bufferSize: Int = DEFAULT_BUFFER_SIZE +) : Readable by source { + + /** + * The buffer containing the current bytes read from the wrapped [Readable], with the range it + * covers. + */ + private var buffer: Pair? = null + + private lateinit var _cachedLength: Try + private suspend fun cachedLength(): Try { + if (!::_cachedLength.isInitialized) { + _cachedLength = source.length() + } + return _cachedLength + } + + init { + if (contentLength != null) { + _cachedLength = Try.success(contentLength) + } + } + + override suspend fun read(range: LongRange?): Try { + val length = cachedLength().getOrNull() + // Reading the whole resource bypasses buffering to keep things simple. + if (range == null || length == null) { + return source.read(range) + } + + val requestedRange = range + .coerceIn(0L until length) + .requireLengthFitInt() + if (requestedRange.isEmpty()) { + return Try.success(ByteArray(0)) + } + + // Round up the range to be read to the next `bufferSize`, because we will buffer the + // excess. + val readLast = (requestedRange.last + 1).ceilMultipleOf(bufferSize.toLong()).coerceAtMost( + length + ) + var readRange = requestedRange.first until readLast + + // Attempt to serve parts or all of the request using the buffer. + buffer?.let { pair -> + var (buffer, bufferedRange) = pair + + // Everything already buffered? + if (bufferedRange.contains(requestedRange)) { + val data = extractRange(requestedRange, buffer, start = bufferedRange.first) + return Try.success(data) + + // Beginning of requested data is buffered? + } else if (bufferedRange.contains(requestedRange.first)) { + readRange = (bufferedRange.last + 1)..readRange.last + + return source.read(readRange).map { readData -> + buffer += readData + // Shift the current buffer to the tail of the read data. + saveBuffer(buffer, readRange) + + val bytes = extractRange(requestedRange, buffer, start = bufferedRange.first) + bytes + } + } + } + + // Fallback on reading the requested range from the original resource. + return source.read(readRange).map { data -> + saveBuffer(data, readRange) + + val res = if (data.count() > requestedRange.count()) { + data.copyOfRange(0, requestedRange.count()) + } else { + data + } + + res + } + } + + /** + * Keeps the last chunk of the given data as the buffer for next reads. + * + * @param data Data read from the original resource. + * @param range Range of the read data in the resource. + */ + private fun saveBuffer(data: ByteArray, range: LongRange) { + val lastChunk = data.takeLast(bufferSize).toByteArray() + val chunkRange = (range.last + 1 - lastChunk.count())..range.last + buffer = Pair(lastChunk, chunkRange) + } + + /** + * Reads a sub-range of the given [data] after shifting the given absolute (to the resource) + * ranges to be relative to [data]. + */ + private fun extractRange(requestedRange: LongRange, data: ByteArray, start: Long): ByteArray { + val first = requestedRange.first - start + val lastExclusive = first + requestedRange.count() + require(first >= 0) + require(lastExclusive <= data.count()) { "$lastExclusive > ${data.count()}" } + return data.copyOfRange(first.toInt(), lastExclusive.toInt()) + } + + private fun Long.ceilMultipleOf(divisor: Long) = + divisor * (this / divisor + if (this % divisor == 0L) 0 else 1) +} + +/** + * Wraps this resource into a buffer to improve reading performances. + * + * Expensive interaction with the underlying resource is minimized, since most (smaller) requests + * can be satisfied by accessing the buffer alone. The drawback is that some extra space is required + * to hold the buffer and that copying takes place when filling that buffer, but this is usually + * outweighed by the performance benefits. + * + * Note that this implementation is pretty limited and the benefits are only apparent when reading + * forward and consecutively – e.g. when downloading the resource by chunks. The buffer is ignored + * when reading backward or far ahead. + * + * @param contentLength The total length of the resource, when known. This can improve performance + * by avoiding requesting the length from the underlying resource. + * @param size Size of the buffer chunks to read. + */ +public fun Readable.buffered( + contentLength: Long? = null, + size: Int = DEFAULT_BUFFER_SIZE +): Readable = + ReadableBuffer(source = this, contentLength = contentLength, bufferSize = size) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt index 0fca54d1b1..9966234031 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -22,11 +22,10 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.file.DirectoryContainer -import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.use +import org.readium.r2.shared.util.zip.FileZipArchiveProvider import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider -import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.robolectric.ParameterizedRobolectricTestRunner @RunWith(ParameterizedRobolectricTestRunner::class) @@ -42,10 +41,10 @@ class ZipContainerTest(val sut: suspend () -> Container) { val zipArchive = suspend { assertNotNull( - ZipArchiveFactory() + FileZipArchiveProvider() .create( mediaType = MediaType.ZIP, - FileResource(File(epubZip.path)) + file = File(epubZip.path) ) .getOrNull() ) @@ -124,6 +123,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { } } + @Suppress("EmptyRange") @Test fun `Decreasing ranges are understood as empty ones`(): Unit = runBlocking { sut().use { container -> From 296c7229715b20bc9ca861192fd17c4eba3dc651 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 6 Dec 2023 16:01:39 +0100 Subject: [PATCH 54/86] Fix LcpFallbackContentProtection --- .../publication/protection/LcpFallbackContentProtection.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index aba8afc318..289c549dcf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -99,7 +99,7 @@ public class LcpFallbackContentProtection : ContentProtection { ?.getOrElse { when (it) { is DecodeError.Reading -> - return Try.failure(ReadError.Decoding(it)) + return Try.failure(it.cause) is DecodeError.Decoding -> return Try.success(false) } @@ -114,13 +114,12 @@ public class LcpFallbackContentProtection : ContentProtection { } private suspend fun hasLcpSchemeInEncryptionXml(container: Container): Try { - val encryptionXml = container - .get(Url("META-INF/encryption.xml")!!) + val encryptionXml = container[Url("META-INF/encryption.xml")!!] ?.readAsXml() ?.getOrElse { when (it) { is DecodeError.Reading -> - return Try.failure(ReadError.Decoding(it.cause.cause)) + return Try.failure(it.cause) is DecodeError.Decoding -> return Try.failure(ReadError.Decoding(it.cause)) } From 10a1f5d1ea926dfc7c79b7234a912015cfdd6989 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 6 Dec 2023 17:00:07 +0100 Subject: [PATCH 55/86] Improve MediaTypeRetriever hint sniffing efficiency --- .../util/mediatype/MediaTypeRetriever.kt | 14 +++++++--- .../SimpleResourceMediaTypeRetriever.kt | 28 ++++++------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt index 574680bda4..6833d41f8d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt @@ -26,7 +26,7 @@ import org.readium.r2.shared.util.use */ public class MediaTypeRetriever( private val mediaTypeSniffer: MediaTypeSniffer, - formatRegistry: FormatRegistry, + private val formatRegistry: FormatRegistry, archiveFactory: ArchiveFactory ) { @@ -88,8 +88,12 @@ public class MediaTypeRetriever( container: Container, hints: MediaTypeHints = MediaTypeHints() ): Try { - simpleResourceMediaTypeRetriever.retrieveSafe(hints) - .let { Try.success(it) } + val unsafeMediaType = simpleResourceMediaTypeRetriever.retrieveUnsafe(hints) + .getOrNull() + + if (unsafeMediaType != null && !formatRegistry.isSuperType(unsafeMediaType)) { + return Try.success(unsafeMediaType) + } mediaTypeSniffer.sniffContainer(container) .onSuccess { return Try.success(it) } @@ -100,7 +104,9 @@ public class MediaTypeRetriever( } } - return simpleResourceMediaTypeRetriever.retrieveUnsafe(hints) + return (unsafeMediaType ?: hints.mediaTypes.firstOrNull()) + ?.let { Try.success(it) } + ?: Try.failure(MediaTypeSnifferError.NotRecognized) } /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SimpleResourceMediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SimpleResourceMediaTypeRetriever.kt index e2a7e71179..4168a33a65 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SimpleResourceMediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SimpleResourceMediaTypeRetriever.kt @@ -7,7 +7,6 @@ package org.readium.r2.shared.util.mediatype import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.filename @@ -22,21 +21,6 @@ internal class SimpleResourceMediaTypeRetriever( private val formatRegistry: FormatRegistry ) { - /** - * Retrieves a canonical [MediaType] for the provided media type [hints]. - * - * Does not recognize media types and file extensions for too generic types. - */ - fun retrieveSafe(hints: MediaTypeHints): Try = - retrieveUnsafe(hints) - .flatMap { - if (formatRegistry.isSuperType(it)) { - Try.failure(MediaTypeSnifferError.NotRecognized) - } else { - Try.success(it) - } - } - /** * Retrieves a [MediaType] as much canonical as possible without accessing the content. */ @@ -62,8 +46,12 @@ internal class SimpleResourceMediaTypeRetriever( ?.substringAfterLast(".", "") ) - retrieveSafe(embeddedHints + hints) - .onSuccess { return Try.success(it) } + val unsafeMediaType = retrieveUnsafe(embeddedHints + hints) + .getOrNull() + + if (unsafeMediaType != null && !formatRegistry.isSuperType(unsafeMediaType)) { + return Try.success(unsafeMediaType) + } mediaTypeSniffer.sniffBlob(resource) .onSuccess { return Try.success(it) } @@ -74,6 +62,8 @@ internal class SimpleResourceMediaTypeRetriever( } } - return retrieveUnsafe(hints) + return (unsafeMediaType ?: hints.mediaTypes.firstOrNull()) + ?.let { Try.success(it) } + ?: Try.failure(MediaTypeSnifferError.NotRecognized) } } From 678238bb5c5015f42311805ebcb0278e4d10a17a Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 6 Dec 2023 17:06:55 +0100 Subject: [PATCH 56/86] Cosmetic changes --- .../{SmartArchiveFactory.kt => RecursiveArchiveFactory.kt} | 6 +++++- .../java/org/readium/r2/shared/util/asset/AssetRetriever.kt | 4 ++-- .../readium/r2/shared/util/mediatype/MediaTypeRetriever.kt | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) rename readium/shared/src/main/java/org/readium/r2/shared/util/archive/{SmartArchiveFactory.kt => RecursiveArchiveFactory.kt} (88%) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt similarity index 88% rename from readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt index 72a8467b31..3785901aa1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/SmartArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt @@ -14,7 +14,11 @@ import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.tryRecover -internal class SmartArchiveFactory( +/** + * Extends an [ArchiveFactory] to accept media types that [formatRegistry] claims to be + * subtypes of the one given in [create]. + */ +internal class RecursiveArchiveFactory( private val archiveFactory: ArchiveFactory, private val formatRegistry: FormatRegistry ) : ArchiveFactory { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 7e7aeb2d60..683f836c7f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -13,7 +13,7 @@ import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.archive.ArchiveFactory -import org.readium.r2.shared.util.archive.SmartArchiveFactory +import org.readium.r2.shared.util.archive.RecursiveArchiveFactory import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType @@ -51,7 +51,7 @@ public class AssetRetriever( } private val archiveFactory: ArchiveFactory = - SmartArchiveFactory(archiveFactory, formatRegistry) + RecursiveArchiveFactory(archiveFactory, formatRegistry) /** * Retrieves an asset from an url and a known media type. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt index 6833d41f8d..f439bf4fde 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt @@ -9,7 +9,7 @@ package org.readium.r2.shared.util.mediatype import java.io.File import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.archive.ArchiveFactory -import org.readium.r2.shared.util.archive.SmartArchiveFactory +import org.readium.r2.shared.util.archive.RecursiveArchiveFactory import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.file.FileResource @@ -34,7 +34,7 @@ public class MediaTypeRetriever( SimpleResourceMediaTypeRetriever(mediaTypeSniffer, formatRegistry) private val archiveFactory: ArchiveFactory = - SmartArchiveFactory(archiveFactory, formatRegistry) + RecursiveArchiveFactory(archiveFactory, formatRegistry) /** * Retrieves a canonical [MediaType] for the provided media type and file extension [hints]. From 77f11a7829e5a8d91ba43edec5c1d4cfea7d68b7 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 6 Dec 2023 18:25:12 +0100 Subject: [PATCH 57/86] Reorganization --- .../readium/r2/shared/util/data/Buffering.kt | 44 +++---- .../readium/r2/shared/util/data/Decoding.kt | 3 - .../r2/shared/util/data/InputStream.kt | 15 --- .../readium/r2/shared/util/data/Readable.kt | 32 ----- .../util/data/{ReadError.kt => Reading.kt} | 40 +++++- .../util/mediatype/MediaTypeRetriever.kt | 115 ++++++++++++------ .../SimpleResourceMediaTypeRetriever.kt | 69 ----------- .../util/mediatype/MediaTypeRetrieverTest.kt | 24 ++-- 8 files changed, 151 insertions(+), 191 deletions(-) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/data/Readable.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/data/{ReadError.kt => Reading.kt} (65%) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SimpleResourceMediaTypeRetriever.kt diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt index 4393f67686..4cd9768906 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt @@ -11,6 +11,28 @@ import org.readium.r2.shared.extensions.contains import org.readium.r2.shared.extensions.requireLengthFitInt import org.readium.r2.shared.util.Try +/** + * Wraps this resource into a buffer to improve reading performances. + * + * Expensive interaction with the underlying resource is minimized, since most (smaller) requests + * can be satisfied by accessing the buffer alone. The drawback is that some extra space is required + * to hold the buffer and that copying takes place when filling that buffer, but this is usually + * outweighed by the performance benefits. + * + * Note that this implementation is pretty limited and the benefits are only apparent when reading + * forward and consecutively – e.g. when downloading the resource by chunks. The buffer is ignored + * when reading backward or far ahead. + * + * @param contentLength The total length of the resource, when known. This can improve performance + * by avoiding requesting the length from the underlying resource. + * @param size Size of the buffer chunks to read. + */ +public fun Readable.buffered( + contentLength: Long? = null, + size: Int = DEFAULT_BUFFER_SIZE +): Readable = + ReadableBuffer(source = this, contentLength = contentLength, bufferSize = size) + /** * Wraps a [Readable] and buffers its content. * @@ -131,25 +153,3 @@ internal class ReadableBuffer internal constructor( private fun Long.ceilMultipleOf(divisor: Long) = divisor * (this / divisor + if (this % divisor == 0L) 0 else 1) } - -/** - * Wraps this resource into a buffer to improve reading performances. - * - * Expensive interaction with the underlying resource is minimized, since most (smaller) requests - * can be satisfied by accessing the buffer alone. The drawback is that some extra space is required - * to hold the buffer and that copying takes place when filling that buffer, but this is usually - * outweighed by the performance benefits. - * - * Note that this implementation is pretty limited and the benefits are only apparent when reading - * forward and consecutively – e.g. when downloading the resource by chunks. The buffer is ignored - * when reading backward or far ahead. - * - * @param contentLength The total length of the resource, when known. This can improve performance - * by avoiding requesting the length from the underlying resource. - * @param size Size of the buffer chunks to read. - */ -public fun Readable.buffered( - contentLength: Long? = null, - size: Int = DEFAULT_BUFFER_SIZE -): Readable = - ReadableBuffer(source = this, contentLength = contentLength, bufferSize = size) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index b1fbf83bee..859746c04c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -75,9 +75,6 @@ internal suspend fun Try.decodeMap( /** * Content as plain text. - * - * It will extract the charset parameter from the media type hints to figure out an encoding. - * Otherwise, fallback on UTF-8. */ public suspend fun Readable.readAsString( charset: Charset = Charsets.UTF_8 diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/InputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/InputStream.kt index c7e70cdebc..9cf44ff8ce 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/InputStream.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/InputStream.kt @@ -155,18 +155,3 @@ private class ReadableInputStreamAdapter( } } } - -/** - * Returns a new [Readable] accessing the same data but not owning them. - */ -public fun Readable.borrow(): Readable = - BorrowedReadable(this) - -private class BorrowedReadable( - private val readable: Readable -) : Readable by readable { - - override suspend fun close() { - // Do nothing - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Readable.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Readable.kt deleted file mode 100644 index 9eb712b2f7..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Readable.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.data - -import org.readium.r2.shared.util.SuspendingCloseable -import org.readium.r2.shared.util.Try - -/** - * Acts as a proxy to an actual data source by handling read access. - */ -public interface Readable : SuspendingCloseable { - - /** - * Returns data length from metadata if available, or calculated from reading the bytes otherwise. - * - * This value must be treated as a hint, as it might not reflect the actual bytes length. To get - * the real length, you need to read the whole resource. - */ - public suspend fun length(): Try - - /** - * Reads the bytes at the given range. - * - * When [range] is null, the whole content is returned. Out-of-range indexes are clamped to the - * available length automatically. - */ - public suspend fun read(range: LongRange? = null): Try -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt similarity index 65% rename from readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt index 5dfba58c7b..382834d196 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/ReadError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt @@ -10,9 +10,34 @@ import java.io.IOException import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try +/** + * Acts as a proxy to an actual data source by handling read access. + */ +public interface Readable : SuspendingCloseable { + + /** + * Returns data length from metadata if available, or calculated from reading the bytes otherwise. + * + * This value must be treated as a hint, as it might not reflect the actual bytes length. To get + * the real length, you need to read the whole resource. + */ + public suspend fun length(): Try + + /** + * Reads the bytes at the given range. + * + * When [range] is null, the whole content is returned. Out-of-range indexes are clamped to the + * available length automatically. + */ + public suspend fun read(range: LongRange? = null): Try +} + +public typealias ReadTry = Try + /** * Errors occurring while reading a resource. */ @@ -61,4 +86,17 @@ public class ReadException( public val error: ReadError ) : IOException(error.message, ErrorException(error)) -public typealias ReadTry = Try +/** + * Returns a new [Readable] accessing the same data but not owning them. + */ +public fun Readable.borrow(): Readable = + BorrowedReadable(this) + +private class BorrowedReadable( + private val readable: Readable +) : Readable by readable { + + override suspend fun close() { + // Do nothing + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt index f439bf4fde..d76303cc2e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt @@ -15,13 +15,16 @@ import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.resource.mediaType +import org.readium.r2.shared.util.tryRecover import org.readium.r2.shared.util.use /** * Retrieves a canonical [MediaType] for the provided media type and file extension hints and * asset content. * - * The actual format sniffing is mostly done by the provided [mediaTypeSniffer]. + * The actual format sniffing is done by the provided [mediaTypeSniffer]. * The [DefaultMediaTypeSniffer] covers the formats supported with Readium by default. */ public class MediaTypeRetriever( @@ -30,9 +33,6 @@ public class MediaTypeRetriever( archiveFactory: ArchiveFactory ) { - private val simpleResourceMediaTypeRetriever: SimpleResourceMediaTypeRetriever = - SimpleResourceMediaTypeRetriever(mediaTypeSniffer, formatRegistry) - private val archiveFactory: ArchiveFactory = RecursiveArchiveFactory(archiveFactory, formatRegistry) @@ -42,7 +42,7 @@ public class MediaTypeRetriever( * Useful for testing purpose. */ internal fun retrieve(hints: MediaTypeHints): MediaType? = - simpleResourceMediaTypeRetriever.retrieveUnsafe(hints) + retrieveUnsafe(hints) .getOrNull() /** @@ -59,24 +59,42 @@ public class MediaTypeRetriever( ) /** - * Retrieves a canonical [MediaType] for the provided [mediaType] and [fileExtension] hints. + * Retrieves a canonical [MediaType] for [resource]. * - * Useful for testing purpose. + * @param resource the resource to retrieve the media type of + * @param hints additional hints which will be added to those provided by the resource */ + public suspend fun retrieve( + resource: Resource, + hints: MediaTypeHints = MediaTypeHints() + ): Try { + val resourceMediaType = retrieveUnsafe(resource, hints) + .getOrElse { return Try.failure(it) } + + val container = archiveFactory.create(resourceMediaType, resource) + .getOrElse { + when (it) { + is ArchiveFactory.Error.Reading -> + return Try.failure(MediaTypeSnifferError.Reading(it.cause)) + is ArchiveFactory.Error.FormatNotSupported -> + return Try.success(resourceMediaType) + } + } - internal fun retrieve(mediaType: MediaType, fileExtension: String? = null): MediaType = - retrieve(MediaTypeHints(mediaType = mediaType, fileExtension = fileExtension)) ?: mediaType + return retrieve(container, hints) + } /** - * Retrieves a canonical [MediaType] for the provided [mediaTypes] and [fileExtensions] hints. + * Retrieves a canonical [MediaType] for [file]. * - * Useful for testing purpose. + * @param file the file to retrieve the media type of + * @param hints additional hints which will be added to those provided by the resource */ - internal fun retrieve( - mediaTypes: List = emptyList(), - fileExtensions: List = emptyList() - ): MediaType? = - retrieve(MediaTypeHints(mediaTypes = mediaTypes, fileExtensions = fileExtensions)) + public suspend fun retrieve( + file: File, + hints: MediaTypeHints = MediaTypeHints() + ): Try = + FileResource(file).use { retrieve(it, hints) } /** * Retrieves a canonical [MediaType] for [container]. @@ -88,7 +106,7 @@ public class MediaTypeRetriever( container: Container, hints: MediaTypeHints = MediaTypeHints() ): Try { - val unsafeMediaType = simpleResourceMediaTypeRetriever.retrieveUnsafe(hints) + val unsafeMediaType = retrieveUnsafe(hints) .getOrNull() if (unsafeMediaType != null && !formatRegistry.isSuperType(unsafeMediaType)) { @@ -110,40 +128,57 @@ public class MediaTypeRetriever( } /** - * Retrieves a canonical [MediaType] for [file]. + * Retrieves a [MediaType] as much canonical as possible without accessing the content. * - * @param file the file to retrieve the media type of - * @param hints additional hints which will be added to those provided by the resource + * Does not refuse too generic types. */ - public suspend fun retrieve( - file: File, - hints: MediaTypeHints = MediaTypeHints() - ): Try = - FileResource(file).use { retrieve(it, hints) } + private fun retrieveUnsafe( + hints: MediaTypeHints + ): Try = + mediaTypeSniffer.sniffHints(hints) + .tryRecover { + hints.mediaTypes.firstOrNull() + ?.let { Try.success(it) } + ?: Try.failure(MediaTypeSnifferError.NotRecognized) + } /** - * Retrieves a canonical [MediaType] for [resource]. + * Retrieves a [MediaType] for [resource] using [hints] added to those embedded in [resource] + * and reading content if necessary. * - * @param resource the resource to retrieve the media type of - * @param hints additional hints which will be added to those provided by the resource + * Does not open archive resources. */ - public suspend fun retrieve( + private suspend fun retrieveUnsafe( resource: Resource, - hints: MediaTypeHints = MediaTypeHints() + hints: MediaTypeHints ): Try { - val resourceMediaType = simpleResourceMediaTypeRetriever.retrieve(resource, hints) - .getOrElse { return Try.failure(it) } + val properties = resource.properties() + .getOrElse { return Try.failure(MediaTypeSnifferError.Reading(it)) } - val container = archiveFactory.create(resourceMediaType, resource) - .getOrElse { - when (it) { - is ArchiveFactory.Error.Reading -> - return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - is ArchiveFactory.Error.FormatNotSupported -> - return Try.success(resourceMediaType) + val embeddedHints = MediaTypeHints( + mediaType = properties.mediaType, + fileExtension = properties.filename + ?.substringAfterLast(".", "") + ) + + val unsafeMediaType = retrieveUnsafe(embeddedHints + hints) + .getOrNull() + + if (unsafeMediaType != null && !formatRegistry.isSuperType(unsafeMediaType)) { + return Try.success(unsafeMediaType) + } + + mediaTypeSniffer.sniffBlob(resource) + .onSuccess { return Try.success(it) } + .onFailure { error -> + when (error) { + is MediaTypeSnifferError.NotRecognized -> {} + else -> return Try.failure(error) } } - return retrieve(container, hints) + return (unsafeMediaType ?: hints.mediaTypes.firstOrNull()) + ?.let { Try.success(it) } + ?: Try.failure(MediaTypeSnifferError.NotRecognized) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SimpleResourceMediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SimpleResourceMediaTypeRetriever.kt deleted file mode 100644 index 4168a33a65..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/SimpleResourceMediaTypeRetriever.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.mediatype - -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.filename -import org.readium.r2.shared.util.resource.mediaType -import org.readium.r2.shared.util.tryRecover - -/** - * A simple [MediaTypeRetriever] which does not open archive resources. - */ -internal class SimpleResourceMediaTypeRetriever( - private val mediaTypeSniffer: MediaTypeSniffer, - private val formatRegistry: FormatRegistry -) { - - /** - * Retrieves a [MediaType] as much canonical as possible without accessing the content. - */ - fun retrieveUnsafe(hints: MediaTypeHints): Try = - mediaTypeSniffer.sniffHints(hints) - .tryRecover { - hints.mediaTypes.firstOrNull() - ?.let { Try.success(it) } - ?: Try.failure(MediaTypeSnifferError.NotRecognized) - } - - /** - * Retrieves a [MediaType] for [resource] using [hints] added to those embedded in [resource] - * and reading content if necessary. - */ - suspend fun retrieve(resource: Resource, hints: MediaTypeHints): Try { - val properties = resource.properties() - .getOrElse { return Try.failure(MediaTypeSnifferError.Reading(it)) } - - val embeddedHints = MediaTypeHints( - mediaType = properties.mediaType, - fileExtension = properties.filename - ?.substringAfterLast(".", "") - ) - - val unsafeMediaType = retrieveUnsafe(embeddedHints + hints) - .getOrNull() - - if (unsafeMediaType != null && !formatRegistry.isSuperType(unsafeMediaType)) { - return Try.success(unsafeMediaType) - } - - mediaTypeSniffer.sniffBlob(resource) - .onSuccess { return Try.success(it) } - .onFailure { error -> - when (error) { - is MediaTypeSnifferError.NotRecognized -> {} - else -> return Try.failure(error) - } - } - - return (unsafeMediaType ?: hints.mediaTypes.firstOrNull()) - ?.let { Try.success(it) } - ?: Try.failure(MediaTypeSnifferError.NotRecognized) - } -} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt index 35d6b8059f..09d5c47b24 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt @@ -71,8 +71,10 @@ class MediaTypeRetrieverTest { assertEquals( MediaType.READIUM_AUDIOBOOK, retriever.retrieve( - mediaTypes = listOf("application/audiobook+zip"), - fileExtensions = listOf("audiobook") + MediaTypeHints( + mediaTypes = listOf("application/audiobook+zip"), + fileExtensions = listOf("audiobook") + ) ) ) } @@ -496,18 +498,22 @@ class MediaTypeRetrieverTest { assertEquals( xlsx, retriever.retrieve( - mediaTypes = emptyList(), - fileExtensions = listOf("foobar", "xlsx") + MediaTypeHints( + mediaTypes = emptyList(), + fileExtensions = listOf("foobar", "xlsx") + ) ) ) assertEquals( xlsx, retriever.retrieve( - mediaTypes = listOf( - "applicaton/foobar", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ), - fileExtensions = emptyList() + MediaTypeHints( + mediaTypes = listOf( + "applicaton/foobar", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ), + fileExtensions = emptyList() + ) ) ) } From db48f8de21885a0286fe73fe2f9106a5ea8fb8ed Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 6 Dec 2023 19:16:35 +0100 Subject: [PATCH 58/86] Doc --- .../readium/r2/shared/util/data/Reading.kt | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt index 382834d196..4bcdf1bb7b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt @@ -46,9 +46,21 @@ public sealed class ReadError( override val cause: Error? = null ) : Error { + /** + * An error occurred while trying to access the content. + * + * At the moment, [AccessError]s constructed by the toolkit can be either a FileSystemError, + * a ContentResolverError or an HttpError. + */ public class Access(public override val cause: AccessError) : ReadError("An error occurred while attempting to access data.", cause) + /** + * Content doesn't match what was expected and cannot be interpreted. + * + * For instance, this error can be reported if an ZIP archive looks invalid, + * a publication doesn't conform to its format, or a JSON resource cannot be decoded. + */ public class Decoding(cause: Error? = null) : ReadError("An error occurred while attempting to decode the content.", cause) { @@ -56,12 +68,25 @@ public sealed class ReadError( public constructor(exception: Exception) : this(ThrowableError(exception)) } + /** + * Content could not be successfully read because there is not enough memory available. + * + * This error can be produced while trying to put the content into memory or while + * trying to decode it. + */ public class OutOfMemory(override val cause: ThrowableError) : ReadError("The resource is too large to be read on this device.", cause) { public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) } + /** + * An operation could not be performed at some point. + * + * For instance, this error can occur no matter the level of indirection when trying + * to read ranges of getting length if any component the data has to pass through + * doesn't support that. + */ public class UnsupportedOperation(cause: Error? = null) : ReadError("Could not proceed because an operation was not supported.", cause) { @@ -71,9 +96,6 @@ public sealed class ReadError( /** * Marker interface for source-specific access errors. - * - * At the moment, [AccessError]s constructed by the toolkit can be either a FileSystemError, - * a ContentResolverError or an HttpError. */ public interface AccessError : Error From 632c8ca7b1c848fa2d321a9b7fc7d15ebb745b0e Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 6 Dec 2023 20:28:25 +0100 Subject: [PATCH 59/86] Cosmetic changes --- .../java/org/readium/r2/shared/util/data/Decoding.kt | 9 +++++---- .../main/java/org/readium/r2/shared/util/data/Reading.kt | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index 859746c04c..d52c1ed999 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -23,16 +23,17 @@ import org.readium.r2.shared.util.xml.ElementNode import org.readium.r2.shared.util.xml.XmlParser public sealed class DecodeError( - override val message: String + override val message: String, + override val cause: Error ) : Error { public class Reading( override val cause: ReadError - ) : DecodeError("Reading error") + ) : DecodeError("Reading error", cause) public class Decoding( - override val cause: Error? - ) : DecodeError("Decoding Error") + cause: Error + ) : DecodeError("Decoding Error", cause) } internal suspend fun Try.decode( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt index 4bcdf1bb7b..5245eb46f3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt @@ -43,7 +43,7 @@ public typealias ReadTry = Try */ public sealed class ReadError( override val message: String, - override val cause: Error? = null + override val cause: Error ) : Error { /** @@ -61,7 +61,7 @@ public sealed class ReadError( * For instance, this error can be reported if an ZIP archive looks invalid, * a publication doesn't conform to its format, or a JSON resource cannot be decoded. */ - public class Decoding(cause: Error? = null) : + public class Decoding(cause: Error) : ReadError("An error occurred while attempting to decode the content.", cause) { public constructor(message: String) : this(DebugError(message)) @@ -84,10 +84,10 @@ public sealed class ReadError( * An operation could not be performed at some point. * * For instance, this error can occur no matter the level of indirection when trying - * to read ranges of getting length if any component the data has to pass through + * to read ranges or getting length if any component the data has to pass through * doesn't support that. */ - public class UnsupportedOperation(cause: Error? = null) : + public class UnsupportedOperation(cause: Error) : ReadError("Could not proceed because an operation was not supported.", cause) { public constructor(message: String) : this(DebugError(message)) From fce3d33f6697a4e06244c230a04f2cdd89d202a2 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 6 Dec 2023 20:43:39 +0100 Subject: [PATCH 60/86] Cosmetic changes --- .../adapter/pdfium/document/PdfiumDocument.kt | 2 +- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 2 +- .../org/readium/r2/lcp/LcpDecryptorTest.kt | 4 +- .../container/ContentZipLicenseContainer.kt | 2 +- .../lcp/license/container/LicenseContainer.kt | 10 ++--- .../readium/r2/navigator/epub/HtmlInjector.kt | 2 +- .../util/archive/RecursiveArchiveFactory.kt | 2 +- .../r2/shared/util/content/ContentResource.kt | 2 +- .../readium/r2/shared/util/data/Buffering.kt | 6 +-- .../readium/r2/shared/util/data/Container.kt | 2 +- .../r2/shared/util/file/FileResource.kt | 2 +- .../r2/shared/util/http/HttpResource.kt | 10 ++--- .../readium/r2/shared/util/pdf/PdfDocument.kt | 2 +- .../shared/util/resource/BufferingResource.kt | 6 +-- .../shared/util/resource/FallbackResource.kt | 2 +- .../shared/util/resource/InMemoryResource.kt | 4 +- .../r2/shared/util/resource/LazyResource.kt | 2 +- .../r2/shared/util/resource/Resource.kt | 16 ++++---- .../r2/shared/util/resource/StringResource.kt | 3 -- .../util/resource/SynchronizedResource.kt | 2 +- .../util/resource/TransformingResource.kt | 2 +- .../r2/shared/util/zip/FileZipContainer.kt | 4 +- ...leChannel.kt => ReadableChannelAdapter.kt} | 2 +- .../util/zip/StreamingZipArchiveProvider.kt | 4 +- .../shared/util/zip/StreamingZipContainer.kt | 4 +- .../r2/shared/util/zip/ZipArchiveFactory.kt | 2 +- .../r2/shared/util/zip/ZipMediaTypeSniffer.kt | 2 +- .../util/resource/BufferingResourceTest.kt | 2 +- .../readium/r2/streamer/ParserAssetFactory.kt | 37 ++++++++++--------- .../readium/r2/streamer/PublicationFactory.kt | 4 +- .../streamer/parser/epub/EpubDeobfuscator.kt | 2 +- .../parser/epub/EpubDeobfuscatorTest.kt | 2 +- .../parser/epub/EpubPositionsServiceTest.kt | 2 +- 33 files changed, 75 insertions(+), 77 deletions(-) rename readium/shared/src/main/java/org/readium/r2/shared/util/zip/{ReadableChannel.kt => ReadableChannelAdapter.kt} (98%) diff --git a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt index 21518d3314..6a388c6454 100644 --- a/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt +++ b/readium/adapters/pdfium/document/src/main/java/org/readium/adapter/pdfium/document/PdfiumDocument.kt @@ -96,7 +96,7 @@ public class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory? = tryOrNull { - source?.toFile()?.let { file -> + sourceUrl?.toFile()?.let { file -> withContext(Dispatchers.IO) { Try.success(core.fromFile(file, password)) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 61e06d4acb..759f1e5f0c 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -191,7 +191,7 @@ internal class LcpDecryptor( return Try.failure( ReadError.Decoding( DebugError( - "Can't decrypt the content for resource with key: ${resource.source}", + "Can't decrypt the content for resource with key: ${resource.sourceUrl}", it ) ) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt index 5d4dbebdb9..bc38a882df 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptorTest.kt @@ -150,7 +150,7 @@ internal suspend fun Resource.readByChunks( Timber.d("block index ${it.first}: ${it.second}") val decryptedBytes = read(it.second).getOrElse { error -> throw IllegalStateException( - "unable to decrypt chunk ${it.second} from $source", + "unable to decrypt chunk ${it.second} from $sourceUrl", ErrorException(error) ) } @@ -160,7 +160,7 @@ internal suspend fun Resource.readByChunks( Timber.d( "expected length: ${groundTruth.sliceArray(it.second.map(Long::toInt)).size}" ) - "decrypted chunk ${it.first}: ${it.second} seems to be wrong in $source" + "decrypted chunk ${it.first}: ${it.second} seems to be wrong in $sourceUrl" } Pair(it.first, decryptedBytes) } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt index 1816221f32..a09ab849af 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/ContentZipLicenseContainer.kt @@ -29,7 +29,7 @@ internal class ContentZipLicenseContainer( ) : LicenseContainer by ContainerLicenseContainer(container, pathInZip), WritableLicenseContainer { private val zipUri: Uri = - requireNotNull(container.source).toUri() + requireNotNull(container.sourceUrl).toUri() private val contentResolver: ContentResolver = context.contentResolver diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt index 65b7f2b60f..3d8688dd42 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt @@ -66,8 +66,8 @@ internal fun createLicenseContainer( } return when { - resource.source?.isFile == true -> - LcplLicenseContainer(resource.source!!.toFile()!!) + resource.sourceUrl?.isFile == true -> + LcplLicenseContainer(resource.sourceUrl!!.toFile()!!) else -> LcplResourceLicenseContainer(resource) } @@ -85,9 +85,9 @@ internal fun createLicenseContainer( } return when { - container.source?.isFile == true -> - FileZipLicenseContainer(container.source!!.path!!, licensePath) - container.source?.isContent == true -> + container.sourceUrl?.isFile == true -> + FileZipLicenseContainer(container.sourceUrl!!.path!!, licensePath) + container.sourceUrl?.isContent == true -> ContentZipLicenseContainer(context, container, licensePath) else -> ContainerLicenseContainer(container, licensePath) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt index 28ad1f3f31..92faf7169d 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/HtmlInjector.kt @@ -71,7 +71,7 @@ internal fun Resource.injectHtml( val headEndIndex = content.indexOf("", 0, true) if (headEndIndex == -1) { - Timber.e(" closing tag not found in resource with href: $source") + Timber.e(" closing tag not found in resource with href: $sourceUrl") } else { content = StringBuilder(content) .insert(headEndIndex, "\n" + injectables.joinToString("\n") + "\n") diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt index 3785901aa1..05c49d4681 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt @@ -15,7 +15,7 @@ import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.tryRecover /** - * Extends an [ArchiveFactory] to accept media types that [formatRegistry] claims to be + * Decorates an [ArchiveFactory] to accept media types that [formatRegistry] claims to be * subtypes of the one given in [create]. */ internal class RecursiveArchiveFactory( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt index 370baeb5ba..70d9eed2dc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt @@ -44,7 +44,7 @@ public class ContentResource( private lateinit var _properties: Try - override val source: AbsoluteUrl? = uri.toUrl() as? AbsoluteUrl + override val sourceUrl: AbsoluteUrl? = uri.toUrl() as? AbsoluteUrl override suspend fun close() { } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt index 4cd9768906..e38f64e029 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Buffering.kt @@ -25,13 +25,13 @@ import org.readium.r2.shared.util.Try * * @param contentLength The total length of the resource, when known. This can improve performance * by avoiding requesting the length from the underlying resource. - * @param size Size of the buffer chunks to read. + * @param bufferSize Size of the buffer chunks to read. */ public fun Readable.buffered( contentLength: Long? = null, - size: Int = DEFAULT_BUFFER_SIZE + bufferSize: Int = DEFAULT_BUFFER_SIZE ): Readable = - ReadableBuffer(source = this, contentLength = contentLength, bufferSize = size) + ReadableBuffer(source = this, contentLength = contentLength, bufferSize = bufferSize) /** * Wraps a [Readable] and buffers its content. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index c2d68a3e4c..1188b9586f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -24,7 +24,7 @@ public interface Container : Iterable, SuspendingCloseabl /** * Direct source to this container, when available. */ - public val source: AbsoluteUrl? get() = null + public val sourceUrl: AbsoluteUrl? get() = null /** * List of all the container entries. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt index 4d99d6d75f..ec33059a0f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt @@ -54,7 +54,7 @@ public class FileResource( } ) - override val source: AbsoluteUrl = file.toUrl() + override val sourceUrl: AbsoluteUrl = file.toUrl() public override suspend fun properties(): Try { return Try.success(properties) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt index c0c8adb51c..4f71d06c94 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResource.kt @@ -26,7 +26,7 @@ import org.readium.r2.shared.util.resource.mediaType /** Provides access to an external URL through HTTP. */ @OptIn(ExperimentalReadiumApi::class) public class HttpResource( - override val source: AbsoluteUrl, + override val sourceUrl: AbsoluteUrl, private val client: HttpClient, private val maxSkipBytes: Long = MAX_SKIP_BYTES ) : Resource { @@ -51,7 +51,7 @@ public class HttpResource( Try.failure( ReadError.UnsupportedOperation( DebugError( - "Server did not provide content length in its response to request to $source." + "Server did not provide content length in its response to request to $sourceUrl." ) ) ) @@ -84,7 +84,7 @@ public class HttpResource( return _headResponse } - _headResponse = client.head(HttpRequest(source)) + _headResponse = client.head(HttpRequest(sourceUrl)) .mapFailure { ReadError.Access(it) } return _headResponse @@ -109,7 +109,7 @@ public class HttpResource( } tryOrLog { inputStream?.close() } - val request = HttpRequest(source) { + val request = HttpRequest(sourceUrl) { from?.let { setRange(from..-1) } } @@ -118,7 +118,7 @@ public class HttpResource( .flatMap { response -> if (from != null && response.response.statusCode.code != 206) { val error = DebugError( - "Server seems not to support range requests to $source." + "Server seems not to support range requests to $sourceUrl." ) Try.failure(ReadError.UnsupportedOperation(error)) } else { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt index 82de3bb5d9..fdd56a1b48 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/pdf/PdfDocument.kt @@ -57,7 +57,7 @@ private class CachingPdfDocumentFactory( ) : PdfDocumentFactory by factory { override suspend fun open(resource: Resource, password: String?): ReadTry { - val key = resource.source?.toString() ?: return factory.open(resource, password) + val key = resource.sourceUrl?.toString() ?: return factory.open(resource, password) return cache.transaction { getOrTryPut(key) { factory.open(resource, password) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt index 74c6c139f5..421a176193 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/BufferingResource.kt @@ -45,10 +45,10 @@ public class BufferingResource( * * @param resourceLength The total length of the resource, when known. This can improve performance * by avoiding requesting the length from the underlying resource. - * @param size Size of the buffer chunks to read. + * @param bufferSize Size of the buffer chunks to read. */ public fun Resource.buffered( resourceLength: Long? = null, - size: Int = DEFAULT_BUFFER_SIZE + bufferSize: Int = DEFAULT_BUFFER_SIZE ): BufferingResource = - BufferingResource(resource = this, resourceLength = resourceLength, bufferSize = size) + BufferingResource(resource = this, resourceLength = resourceLength, bufferSize = bufferSize) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt index b603847d1d..037f05ce3c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/FallbackResource.kt @@ -18,7 +18,7 @@ public class FallbackResource( private val fallbackResourceFactory: (ReadError) -> Resource? ) : Resource { - override val source: AbsoluteUrl? = null + override val sourceUrl: AbsoluteUrl? = null override suspend fun properties(): Try = withResource { properties() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/InMemoryResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/InMemoryResource.kt index 7828283e3b..e85e2d379b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/InMemoryResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/InMemoryResource.kt @@ -16,7 +16,7 @@ import org.readium.r2.shared.util.data.ReadError /** Creates a [Resource] serving a [ByteArray]. */ public class InMemoryResource( - override val source: AbsoluteUrl?, + override val sourceUrl: AbsoluteUrl?, private val properties: Resource.Properties, private val bytes: suspend () -> Try ) : Resource { @@ -25,7 +25,7 @@ public class InMemoryResource( bytes: ByteArray, source: AbsoluteUrl? = null, properties: Resource.Properties = Resource.Properties() - ) : this(source = source, properties = properties, { Try.success(bytes) }) + ) : this(sourceUrl = source, properties = properties, { Try.success(bytes) }) private lateinit var _bytes: Try diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt index 9c073db2a5..11d6eddf05 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/LazyResource.kt @@ -14,7 +14,7 @@ import org.readium.r2.shared.util.data.ReadError * Wraps a [Resource] which will be created only when first accessing one of its members. */ public open class LazyResource( - override val source: AbsoluteUrl? = null, + override val sourceUrl: AbsoluteUrl? = null, private val factory: suspend () -> Resource ) : Resource { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index f8da39ef4e..5dbf5e1f54 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -20,7 +20,7 @@ public interface Resource : Readable { /** * URL locating this resource, if any. */ - public val source: AbsoluteUrl? + public val sourceUrl: AbsoluteUrl? /** * Properties associated to the resource. @@ -51,7 +51,7 @@ public class FailureResource( private val error: ReadError ) : Resource { - override val source: AbsoluteUrl? = null + override val sourceUrl: AbsoluteUrl? = null override suspend fun properties(): Try = Try.failure(error) override suspend fun length(): Try = Try.failure(error) override suspend fun read(range: LongRange?): Try = Try.failure(error) @@ -61,6 +61,12 @@ public class FailureResource( "${javaClass.simpleName}($error)" } +/** + * Returns a new [Resource] accessing the same data but not owning them. + */ +public fun Resource.borrow(): Resource = + BorrowedResource(this) + private class BorrowedResource( private val resource: Resource ) : Resource by resource { @@ -70,12 +76,6 @@ private class BorrowedResource( } } -/** - * Returns a new [Resource] accessing the same data but not owning them. - */ -public fun Resource.borrow(): Resource = - BorrowedResource(this) - @Deprecated( "Catch exceptions yourself to the most suitable ReadError.", level = DeprecationLevel.ERROR, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt index e610bfc27b..149bb8ebcc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/StringResource.kt @@ -28,9 +28,6 @@ public class StringResource private constructor( properties: Resource.Properties = Resource.Properties() ) : this(source, properties, { Try.success(string) }) - override val source: AbsoluteUrl? = - null - override fun toString(): String = "${javaClass.simpleName}(${runBlocking { read().map { it.decodeToString() } } }})" } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt index 422c07202e..cfb2f55d62 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/SynchronizedResource.kt @@ -23,7 +23,7 @@ public class SynchronizedResource( private val mutex = Mutex() - override val source: AbsoluteUrl? get() = resource.source + override val sourceUrl: AbsoluteUrl? get() = resource.sourceUrl override suspend fun properties(): Try = mutex.withLock { resource.properties() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt index 2f7dcb6547..bf2172aa60 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/TransformingResource.kt @@ -47,7 +47,7 @@ public abstract class TransformingResource( } } - override val source: AbsoluteUrl? = null + override val sourceUrl: AbsoluteUrl? = null private lateinit var _bytes: Try diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt index 8bd703b9cf..65a6ddb808 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt @@ -39,7 +39,7 @@ internal class FileZipContainer( private inner class Entry(private val url: Url, private val entry: ZipEntry) : Resource { - override val source: AbsoluteUrl? = null + override val sourceUrl: AbsoluteUrl? = null override suspend fun properties(): Try = Try.success( @@ -129,7 +129,7 @@ internal class FileZipContainer( override val archiveMediaType: MediaType = MediaType.ZIP - override val source: AbsoluteUrl = file.toUrl() + override val sourceUrl: AbsoluteUrl = file.toUrl() override val entries: Set = tryOrLog { archive.entries().toList() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ReadableChannel.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ReadableChannelAdapter.kt similarity index 98% rename from readium/shared/src/main/java/org/readium/r2/shared/util/zip/ReadableChannel.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/zip/ReadableChannelAdapter.kt index 6941acc6e8..4a6aae8edd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ReadableChannel.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ReadableChannelAdapter.kt @@ -21,7 +21,7 @@ import org.readium.r2.shared.util.zip.jvm.ClosedChannelException import org.readium.r2.shared.util.zip.jvm.NonWritableChannelException import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel -internal class ReadableChannel( +internal class ReadableChannelAdapter( private val readable: Readable, private val wrapError: (ReadError) -> IOException ) : SeekableByteChannel { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 39b0f1886d..0df72d0585 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -59,7 +59,7 @@ internal class StreamingZipArchiveProvider { val container = openBlob( readable, ::ReadException, - (readable as? Resource)?.source + (readable as? Resource)?.sourceUrl ) Try.success(container) } catch (exception: Exception) { @@ -77,7 +77,7 @@ internal class StreamingZipArchiveProvider { wrapError: (ReadError) -> IOException, sourceUrl: AbsoluteUrl? ): Container = withContext(Dispatchers.IO) { - val datasourceChannel = ReadableChannel(readable, wrapError) + val datasourceChannel = ReadableChannelAdapter(readable, wrapError) val channel = wrapBaseChannel(datasourceChannel) val zipFile = ZipFile(channel, true) StreamingZipContainer(zipFile, sourceUrl) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt index 37615aa6ee..13c754958a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt @@ -32,7 +32,7 @@ import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile internal class StreamingZipContainer( private val zipFile: ZipFile, - override val source: AbsoluteUrl? + override val sourceUrl: AbsoluteUrl? ) : Container { private inner class Entry( @@ -40,7 +40,7 @@ internal class StreamingZipContainer( private val entry: ZipArchiveEntry ) : Resource { - override val source: AbsoluteUrl? get() = null + override val sourceUrl: AbsoluteUrl? get() = null override suspend fun properties(): ReadTry = Try.success( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt index cc2117c93d..e5f6caa2e1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt @@ -23,7 +23,7 @@ public class ZipArchiveFactory : ArchiveFactory { mediaType: MediaType, source: Readable ): Try, ArchiveFactory.Error> = - (source as? Resource)?.source?.toFile() + (source as? Resource)?.sourceUrl?.toFile() ?.let { fileZipArchiveProvider.create(mediaType, it) } ?: streamingZipArchiveProvider.create(mediaType, source) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt index 88bf96484e..1bf9c12d00 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt @@ -34,7 +34,7 @@ public object ZipMediaTypeSniffer : MediaTypeSniffer { } override suspend fun sniffBlob(source: Readable): Try { - (source as? Resource)?.source?.toFile() + (source as? Resource)?.sourceUrl?.toFile() ?.let { return fileZipArchiveProvider.sniffFile(it) } return streamingZipArchiveProvider.sniffBlob(source) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt index 265e044aab..77a0b2960e 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/BufferingResourceTest.kt @@ -17,7 +17,7 @@ class BufferingResourceTest { @Test fun `get file`() { - assertEquals(file, sut().source?.toFile()) + assertEquals(file, sut().sourceUrl?.toFile()) } @Test diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index 4132c476c4..2ea80042b8 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -9,6 +9,7 @@ package org.readium.r2.streamer import org.readium.r2.shared.extensions.addPrefix import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.DebugError +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset @@ -32,23 +33,23 @@ internal class ParserAssetFactory( private val formatRegistry: FormatRegistry ) { - sealed class Error( + sealed class CreateError( override val message: String, - override val cause: org.readium.r2.shared.util.Error? - ) : org.readium.r2.shared.util.Error { + override val cause: Error? + ) : Error { - class ReadError( - override val cause: org.readium.r2.shared.util.data.ReadError - ) : Error("An error occurred while trying to read asset.", cause) + class Reading( + override val cause: ReadError + ) : CreateError("An error occurred while trying to read asset.", cause) - class UnsupportedAsset( - override val cause: org.readium.r2.shared.util.Error? - ) : Error("Asset is not supported.", cause) + class FormatNotSupported( + override val cause: Error? + ) : CreateError("Asset is not supported.", cause) } suspend fun createParserAsset( asset: Asset - ): Try { + ): Try { return when (asset) { is ContainerAsset -> createParserAssetForContainer(asset) @@ -59,7 +60,7 @@ internal class ParserAssetFactory( private fun createParserAssetForContainer( asset: ContainerAsset - ): Try = + ): Try = Try.success( PublicationParser.Asset( mediaType = asset.mediaType, @@ -69,7 +70,7 @@ internal class ParserAssetFactory( private suspend fun createParserAssetForResource( asset: ResourceAsset - ): Try = + ): Try = if (asset.mediaType.isRwpm) { createParserAssetForManifest(asset) } else { @@ -78,7 +79,7 @@ internal class ParserAssetFactory( private suspend fun createParserAssetForManifest( asset: ResourceAsset - ): Try { + ): Try { val manifest = asset.resource.readAsRwpm() .mapFailure { when (it) { @@ -86,22 +87,22 @@ internal class ParserAssetFactory( is DecodeError.Reading -> it.cause } } - .getOrElse { return Try.failure(Error.ReadError(it)) } + .getOrElse { return Try.failure(CreateError.Reading(it)) } val baseUrl = manifest.linkWithRel("self")?.href?.resolve() if (baseUrl == null) { - Timber.w("No self link found in the manifest at ${asset.resource.source}") + Timber.w("No self link found in the manifest at ${asset.resource.sourceUrl}") } else { if (baseUrl !is AbsoluteUrl) { return Try.failure( - Error.ReadError( + CreateError.Reading( ReadError.Decoding("Self link is not absolute.") ) ) } if (!baseUrl.isHttp) { return Try.failure( - Error.UnsupportedAsset( + CreateError.FormatNotSupported( DebugError("Self link doesn't use the HTTP(S) scheme.") ) ) @@ -131,7 +132,7 @@ internal class ParserAssetFactory( private fun createParserAssetForContent( asset: ResourceAsset - ): Try { + ): Try { // Historically, the reading order of a standalone file contained a single link with the // HREF "/$assetName". This was fragile if the asset named changed, or was different on // other devices. To avoid this, we now use a single link with the HREF diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 885e7b923d..81de16a61d 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -199,9 +199,9 @@ public class PublicationFactory( val parserAsset = parserAssetFactory.createParserAsset(asset) .mapFailure { when (it) { - is ParserAssetFactory.Error.ReadError -> + is ParserAssetFactory.CreateError.Reading -> OpenError.Reading(it.cause) - is ParserAssetFactory.Error.UnsupportedAsset -> + is ParserAssetFactory.CreateError.FormatNotSupported -> OpenError.FormatNotSupported(it.cause) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt index 3f485fb4dc..15c84da219 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt @@ -27,7 +27,7 @@ internal class EpubDeobfuscator( @Suppress("Unused_parameter") fun transform(url: Url, resource: Resource): Resource = resource.flatMap { - val algorithm = resource.source?.let(retrieveEncryption)?.algorithm + val algorithm = resource.sourceUrl?.let(retrieveEncryption)?.algorithm if (algorithm != null && algorithm2length.containsKey(algorithm)) { DeobfuscatingResource(resource, algorithm) } else { diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt index 1fd8ab17aa..9cd82f2b99 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt @@ -45,7 +45,7 @@ class EpubDeobfuscatorTest { private fun deobfuscate(url: Url, resource: Resource, algorithm: String?): Resource { val deobfuscator = EpubDeobfuscator(identifier) { - if (resource.source == it) { + if (resource.sourceUrl == it) { algorithm?.let { Encryption(algorithm = algorithm) } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt index 1eaafba7c0..719db9b7ce 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt @@ -502,7 +502,7 @@ class EpubPositionsServiceTest { return object : Resource { - override val source: AbsoluteUrl? = null + override val sourceUrl: AbsoluteUrl? = null override suspend fun properties(): ReadTry = Try.success(item.resourceProperties) From ec16df2f539af7e934402c5932319a698fd7f050 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Thu, 7 Dec 2023 19:10:43 +0100 Subject: [PATCH 61/86] Refactor decoding --- .../readium/r2/navigator/R2BasicWebView.kt | 6 +- .../AdeptFallbackContentProtection.kt | 37 ++-- .../LcpFallbackContentProtection.kt | 36 ++-- .../publication/services/CoverService.kt | 7 +- .../iterators/HtmlResourceContentIterator.kt | 16 +- .../readium/r2/shared/util/data/Decoding.kt | 190 ++++++++++++------ .../shared/util/mediatype/MediaTypeSniffer.kt | 190 ++++++------------ .../content/ResourceContentExtractor.kt | 11 +- .../readium/r2/streamer/ParserAssetFactory.kt | 18 +- .../r2/streamer/extensions/Container.kt | 5 +- .../r2/streamer/parser/epub/EpubParser.kt | 55 +++-- .../parser/readium/ReadiumWebPubParser.kt | 29 +-- 12 files changed, 289 insertions(+), 311 deletions(-) diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt index eb72c2cc93..817f0d8c9b 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt @@ -47,7 +47,8 @@ import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.readAsString +import org.readium.r2.shared.util.data.decodeString +import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.use @@ -349,7 +350,8 @@ internal open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebV tryOrLog { listener?.resourceAtUrl(absoluteUrl) ?.use { res -> - res.readAsString() + res.read() + .flatMap { it.decodeString() } .map { Jsoup.parse(it) } .getOrNull() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt index 27c34b4ad6..21b48a6f96 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt @@ -14,10 +14,9 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.ContainerAsset -import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.readAsXml -import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.data.decodeXml +import org.readium.r2.shared.util.data.readDecodeOrElse import org.readium.r2.shared.util.mediatype.MediaType /** @@ -68,30 +67,24 @@ public class AdeptFallbackContentProtection : ContentProtection { } asset.container[Url("META-INF/encryption.xml")!!] - ?.readAsXml() - ?.getOrElse { - when (it) { - is DecodeError.Decoding -> - return Try.success(false) - is DecodeError.Reading -> - return Try.failure(it.cause) - } - }?.get("EncryptedData", EpubEncryption.ENC) + ?.readDecodeOrElse( + decode = { it.decodeXml() }, + recoverRead = { return Try.success(false) }, + recoverDecode = { return Try.success(false) } + ) + ?.get("EncryptedData", EpubEncryption.ENC) ?.flatMap { it.get("KeyInfo", EpubEncryption.SIG) } ?.flatMap { it.get("resource", "http://ns.adobe.com/adept") } ?.takeIf { it.isNotEmpty() } ?.let { return Try.success(true) } - return asset.container.get(Url("META-INF/rights.xml")!!) - ?.readAsXml() - ?.getOrElse { - when (it) { - is DecodeError.Decoding -> - return Try.success(false) - is DecodeError.Reading -> - return Try.failure(it.cause) - } - }?.takeIf { it.namespace == "http://ns.adobe.com/adept" } + return asset.container[Url("META-INF/rights.xml")!!] + ?.readDecodeOrElse( + decode = { it.decodeXml() }, + recoverRead = { return Try.success(false) }, + recoverDecode = { return Try.success(false) } + ) + ?.takeIf { it.namespace == "http://ns.adobe.com/adept" } ?.let { Try.success(true) } ?: Try.success(false) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index 289c549dcf..ae52b1cf04 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -17,11 +17,10 @@ import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.readAsRwpm -import org.readium.r2.shared.util.data.readAsXml -import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.data.decodeRwpm +import org.readium.r2.shared.util.data.decodeXml +import org.readium.r2.shared.util.data.readDecodeOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource @@ -95,16 +94,11 @@ public class LcpFallbackContentProtection : ContentProtection { private suspend fun hasLcpSchemeInManifest(container: Container): Try { val manifest = container[Url("manifest.json")!!] - ?.readAsRwpm() - ?.getOrElse { - when (it) { - is DecodeError.Reading -> - return Try.failure(it.cause) - is DecodeError.Decoding -> - return Try.success(false) - } - } - ?: return Try.success(false) + ?.readDecodeOrElse( + decode = { it.decodeRwpm() }, + recoverRead = { return Try.success(false) }, + recoverDecode = { return Try.success(false) } + ) ?: return Try.success(false) val manifestHasLcpScheme = manifest .readingOrder @@ -115,16 +109,10 @@ public class LcpFallbackContentProtection : ContentProtection { private suspend fun hasLcpSchemeInEncryptionXml(container: Container): Try { val encryptionXml = container[Url("META-INF/encryption.xml")!!] - ?.readAsXml() - ?.getOrElse { - when (it) { - is DecodeError.Reading -> - return Try.failure(it.cause) - is DecodeError.Decoding -> - return Try.failure(ReadError.Decoding(it.cause)) - } - } - ?: return Try.success(false) + ?.readDecodeOrElse( + decode = { it.decodeXml() }, + recover = { return Try.failure(it) } + ) ?: return Try.success(false) val hasLcpScheme = encryptionXml .get("EncryptedData", EpubEncryption.ENC) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt index b063abd2ae..0239c692e3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/CoverService.kt @@ -18,7 +18,8 @@ import org.readium.r2.shared.publication.ServiceFactory import org.readium.r2.shared.publication.firstWithRel import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.readAsBitmap +import org.readium.r2.shared.util.data.decodeBitmap +import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.resource.Resource /** @@ -79,7 +80,9 @@ internal class ResourceCoverService( val resource = container[coverUrl] ?: return null - return resource.readAsBitmap() + return resource + .read() + .flatMap { it.decodeBitmap() } .getOrNull() } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt index 0dfec1b0c1..49e956b1fa 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/content/iterators/HtmlResourceContentIterator.kt @@ -34,7 +34,8 @@ import org.readium.r2.shared.publication.services.positionsByReadingOrder import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Language import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.readAsString +import org.readium.r2.shared.util.data.decodeString +import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource @@ -155,11 +156,14 @@ public class HtmlResourceContentIterator internal constructor( private suspend fun parseElements(): ParsedElements = withContext(Dispatchers.Default) { val document = resource.use { res -> - val html = res.readAsString().getOrElse { - val error = DebugError("Failed to read HTML resource", it.cause) - Timber.w(error.toDebugDescription()) - return@withContext ParsedElements() - } + val html = res + .read() + .flatMap { it.decodeString() } + .getOrElse { + val error = DebugError("Failed to read HTML resource", it.cause) + Timber.w(error.toDebugDescription()) + return@withContext ParsedElements() + } Jsoup.parse(html) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index d52c1ed999..5acc6eb10d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -13,81 +13,73 @@ import java.nio.charset.Charset import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.tryRecover import org.readium.r2.shared.util.xml.ElementNode import org.readium.r2.shared.util.xml.XmlParser +/** + * Errors produced when trying to decode content. + */ public sealed class DecodeError( override val message: String, override val cause: Error ) : Error { - public class Reading( - override val cause: ReadError - ) : DecodeError("Reading error", cause) + /** + * Content could not be successfully decoded because there is not enough memory available. + */ + public class OutOfMemory(override val cause: ThrowableError) : + DecodeError("The resource is too large to be read on this device.", cause) { - public class Decoding( - cause: Error - ) : DecodeError("Decoding Error", cause) -} - -internal suspend fun Try.decode( - block: (value: S) -> R, - wrapError: (Exception) -> Error -): Try = - when (this) { - is Try.Success -> - try { - withContext(Dispatchers.Default) { - Try.success(block(value)) - } - } catch (e: Exception) { - Try.failure(DecodeError.Decoding(wrapError(e))) - } catch (e: OutOfMemoryError) { - Try.failure(DecodeError.Reading(ReadError.OutOfMemory(e))) - } - is Try.Failure -> - Try.failure(DecodeError.Reading(value)) + public constructor(error: OutOfMemoryError) : this(ThrowableError(error)) } -internal suspend fun Try.decodeMap( + /** + * Content could not be successfully decoded because it doesn't match what was expected. + */ + public class Decoding(cause: Error) : + DecodeError("Decoding Error", cause) +} + +/** + * Decodes receiver properly wrapping exceptions into [DecodeError]s. + */ +public suspend fun S.decode( block: (value: S) -> R, wrapError: (Exception) -> Error ): Try = - when (this) { - is Try.Success -> - try { - withContext(Dispatchers.Default) { - Try.success(block(value)) - } - } catch (e: Exception) { - Try.failure(DecodeError.Decoding(wrapError(e))) - } catch (e: OutOfMemoryError) { - Try.failure(DecodeError.Reading(ReadError.OutOfMemory(e))) - } - is Try.Failure -> - Try.failure(value) + withContext(Dispatchers.Default) { + try { + Try.success(block(this@decode)) + } catch (e: Exception) { + Try.failure(DecodeError.Decoding(wrapError(e))) + } catch (e: OutOfMemoryError) { + Try.failure(DecodeError.OutOfMemory(e)) + } } /** * Content as plain text. */ -public suspend fun Readable.readAsString( +public suspend fun ByteArray.decodeString( charset: Charset = Charsets.UTF_8 ): Try = - read().decode( + decode( { String(it, charset = charset) }, { DebugError("Content is not a valid $charset string.", ThrowableError(it)) } ) /** Content as an XML document. */ -public suspend fun Readable.readAsXml(): Try = - read().decode( +public suspend fun ByteArray.decodeXml(): Try = + decode( { XmlParser().parse(ByteArrayInputStream(it)) }, { DebugError("Content is not a valid XML document.", ThrowableError(it)) } ) @@ -95,15 +87,19 @@ public suspend fun Readable.readAsXml(): Try = /** * Content parsed from JSON. */ -public suspend fun Readable.readAsJson(): Try = - readAsString().decodeMap( - { JSONObject(it) }, - { DebugError("Content is not valid JSON.", ThrowableError(it)) } - ) +public suspend fun ByteArray.decodeJson(): Try = + decodeString().flatMap { string -> + decode( + { JSONObject(string) }, + { DebugError("Content is not valid JSON.", ThrowableError(it)) } + ) + } -/** Readium Web Publication Manifest parsed from the content. */ -public suspend fun Readable.readAsRwpm(): Try = - readAsJson().flatMap { json -> +/** + * Readium Web Publication Manifest parsed from the content. + * */ +public suspend fun ByteArray.decodeRwpm(): Try = + decodeJson().flatMap { json -> Manifest.fromJSON(json) ?.let { Try.success(it) } ?: Try.failure( @@ -116,15 +112,85 @@ public suspend fun Readable.readAsRwpm(): Try = /** * Reads the full content as a [Bitmap]. */ -public suspend fun Readable.readAsBitmap(): Try = - read() - .mapFailure { DecodeError.Reading(it) } - .flatMap { bytes -> - BitmapFactory.decodeByteArray(bytes, 0, bytes.size) - ?.let { Try.success(it) } - ?: Try.failure( - DecodeError.Decoding( - DebugError("Could not decode resource as a bitmap.") - ) - ) +public suspend fun ByteArray.decodeBitmap(): Try = + decode( + { + BitmapFactory.decodeByteArray(this, 0, size) + ?: throw Exception("BitmapFactory returned null.") + }, + { DebugError("Could not decode content as a bitmap.") } + ) + +@InternalReadiumApi +public suspend inline fun ByteArray.flatDecode( + decode: (value: ByteArray) -> Try +): Try = + decode(this) + .tryRecover { error -> + when (error) { + is DecodeError.OutOfMemory -> + Try.failure(ReadError.OutOfMemory(error.cause)) + + is DecodeError.Decoding -> + Try.failure(ReadError.Decoding(error.cause)) + } } + +@InternalReadiumApi +public suspend inline fun Readable.flatDecode( + decode: (value: ByteArray) -> Try +): Try = + read().flatMap { it.flatDecode(decode) } + +@InternalReadiumApi +public suspend inline fun Try.decodeOrElse( + decode: (value: ByteArray) -> Try, + recover: (DecodeError.Decoding) -> R +): Try = + flatMap { + decode(it) + .tryRecover { error -> + when (error) { + is DecodeError.OutOfMemory -> + Try.failure(ReadError.OutOfMemory(error.cause)) + is DecodeError.Decoding -> + Try.success(recover(error)) + } + } + } + +@InternalReadiumApi +public suspend inline fun ByteArray.decodeOrElse( + decode: (value: ByteArray) -> Try, + recover: (DecodeError.Decoding) -> R +): Try = + decode(this) + .tryRecover { error -> + when (error) { + is DecodeError.OutOfMemory -> + Try.failure(ReadError.OutOfMemory(error.cause)) + is DecodeError.Decoding -> + Try.success(recover(error)) + } + } + +@InternalReadiumApi +public suspend inline fun Readable.readOrElse( + recover: (ReadError) -> ByteArray +): ByteArray = + read().getOrElse(recover) + +@InternalReadiumApi +public suspend inline fun Readable.readDecodeOrElse( + decode: (value: ByteArray) -> Try, + recoverRead: (ReadError) -> R, + recoverDecode: (DecodeError.Decoding) -> R +): R = + read().decodeOrElse(decode, recoverDecode).getOrElse(recoverRead) + +@InternalReadiumApi +public suspend inline fun Readable.readDecodeOrElse( + decode: (value: ByteArray) -> Try, + recover: (ReadError) -> R +): R = + readDecodeOrElse(decode, recover) { recover(ReadError.Decoding(it)) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index b4f1cf5f6e..fffd31a233 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -23,15 +23,15 @@ import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.data.asInputStream import org.readium.r2.shared.util.data.borrow -import org.readium.r2.shared.util.data.readAsJson -import org.readium.r2.shared.util.data.readAsRwpm -import org.readium.r2.shared.util.data.readAsString -import org.readium.r2.shared.util.data.readAsXml +import org.readium.r2.shared.util.data.decodeJson +import org.readium.r2.shared.util.data.decodeRwpm +import org.readium.r2.shared.util.data.decodeString +import org.readium.r2.shared.util.data.decodeXml +import org.readium.r2.shared.util.data.readDecodeOrElse import org.readium.r2.shared.util.getOrDefault import org.readium.r2.shared.util.getOrElse @@ -173,23 +173,16 @@ public object XhtmlMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - source.readAsXml() - .getOrElse { - when (it) { - is DecodeError.Reading -> - return Try.failure( - MediaTypeSnifferError.Reading(it.cause) - ) - is DecodeError.Decoding -> - null - } - } - ?.takeIf { - it.name.lowercase(Locale.ROOT) == "html" && - it.namespace.lowercase(Locale.ROOT).contains("xhtml") - }?.let { - return Try.success(MediaType.XHTML) - } + source.readDecodeOrElse( + decode = { it.decodeXml() }, + recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, + recoverDecode = { null } + )?.takeIf { + it.name.lowercase(Locale.ROOT) == "html" && + it.namespace.lowercase(Locale.ROOT).contains("xhtml") + }?.let { + return Try.success(MediaType.XHTML) + } return Try.failure(MediaTypeSnifferError.NotRecognized) } @@ -214,28 +207,19 @@ public object HtmlMediaTypeSniffer : MediaTypeSniffer { } // [contentAsXml] will fail if the HTML is not a proper XML document, hence the doctype check. - source.readAsXml() - .getOrElse { - when (it) { - is DecodeError.Reading -> - return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - is DecodeError.Decoding -> - null - } - } + source.readDecodeOrElse( + decode = { it.decodeXml() }, + recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, + recoverDecode = { null } + ) ?.takeIf { it.name.lowercase(Locale.ROOT) == "html" } ?.let { return Try.success(MediaType.HTML) } - source.readAsString() - .getOrElse { - when (it) { - is DecodeError.Reading -> - return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - - is DecodeError.Decoding -> - null - } - } + source.readDecodeOrElse( + decode = { it.decodeString() }, + recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, + recoverDecode = { null } + ) ?.takeIf { it.trimStart().take(15).lowercase() == "" } ?.let { return Try.success(MediaType.HTML) } @@ -286,16 +270,11 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { } // OPDS 1 - source.readAsXml() - .getOrElse { - when (it) { - is DecodeError.Reading -> - return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - is DecodeError.Decoding -> - null - } - } - ?.takeIf { it.namespace == "http://www.w3.org/2005/Atom" } + source.readDecodeOrElse( + decode = { it.decodeXml() }, + recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, + recoverDecode = { null } + )?.takeIf { it.namespace == "http://www.w3.org/2005/Atom" } ?.let { xml -> if (xml.name == "feed") { return Try.success(MediaType.OPDS1) @@ -305,15 +284,11 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { } // OPDS 2 - source.readAsRwpm() - .getOrElse { - when (it) { - is DecodeError.Reading -> - return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - is DecodeError.Decoding -> - null - } - } + source.readDecodeOrElse( + decode = { it.decodeRwpm() }, + recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, + recoverDecode = { null } + ) ?.let { rwpm -> if (rwpm.linkWithRel("self")?.mediaType?.matches("application/opds+json") == true ) { @@ -338,16 +313,8 @@ public object OpdsMediaTypeSniffer : MediaTypeSniffer { // OPDS Authentication Document. source.containsJsonKeys("id", "title", "authentication") - .getOrElse { - when (it) { - is DecodeError.Reading -> - return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - - is DecodeError.Decoding -> - null - } - } - ?.takeIf { it } + .getOrElse { return Try.failure(MediaTypeSnifferError.Reading(it)) } + .takeIf { it } ?.let { return Try.success(MediaType.OPDS_AUTHENTICATION) } return Try.failure(MediaTypeSnifferError.NotRecognized) @@ -373,16 +340,8 @@ public object LcpLicenseMediaTypeSniffer : MediaTypeSniffer { } source.containsJsonKeys("id", "issued", "provider", "encryption") - .getOrElse { - when (it) { - is DecodeError.Reading -> - return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - - is DecodeError.Decoding -> - null - } - } - ?.takeIf { it } + .getOrElse { return Try.failure(MediaTypeSnifferError.Reading(it)) } + .takeIf { it } ?.let { return Try.success(MediaType.LCP_LICENSE_DOCUMENT) } return Try.failure(MediaTypeSnifferError.NotRecognized) @@ -468,17 +427,11 @@ public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { } val manifest: Manifest = - source.readAsRwpm() - .getOrElse { - when (it) { - is DecodeError.Reading -> - return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - - is DecodeError.Decoding -> - null - } - } - ?: return Try.failure(MediaTypeSnifferError.NotRecognized) + source.readDecodeOrElse( + decode = { it.decodeRwpm() }, + recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, + recoverDecode = { null } + ) ?: return Try.failure(MediaTypeSnifferError.NotRecognized) if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { return Try.success(MediaType.READIUM_AUDIOBOOK_MANIFEST) @@ -577,16 +530,11 @@ public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { } // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. - val string = source.readAsString() - .getOrElse { - when (it) { - is DecodeError.Reading -> - return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - - is DecodeError.Decoding -> - null - } - } ?: "" + val string = source.readDecodeOrElse( + decode = { it.decodeString() }, + recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, + recoverDecode = { "" } + ) if ( string.contains("@context") && string.contains("https://www.w3.org/ns/wp-context") @@ -617,15 +565,12 @@ public object EpubMediaTypeSniffer : MediaTypeSniffer { override suspend fun sniffContainer(container: Container): Try { val mimetype = container[RelativeUrl("mimetype")!!] - ?.readAsString(charset = Charsets.US_ASCII) - ?.getOrElse { error -> - when (error) { - is DecodeError.Decoding -> - null - is DecodeError.Reading -> - return Try.failure(MediaTypeSnifferError.Reading(error.cause)) - } - }?.trim() + ?.readDecodeOrElse( + decode = { it.decodeString() }, + recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, + recoverDecode = { null } + )?.trim() + if (mimetype == "application/epub+zip") { return Try.success(MediaType.EPUB) } @@ -858,17 +803,11 @@ public object JsonMediaTypeSniffer : MediaTypeSniffer { return Try.failure(MediaTypeSnifferError.NotRecognized) } - source.readAsJson() - .getOrElse { - when (it) { - is DecodeError.Reading -> - return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - - is DecodeError.Decoding -> - null - } - } - ?.let { return Try.success(MediaType.JSON) } + source.readDecodeOrElse( + decode = { it.decodeJson() }, + recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, + recoverDecode = { null } + )?.let { return Try.success(MediaType.JSON) } return Try.failure(MediaTypeSnifferError.NotRecognized) } @@ -945,8 +884,11 @@ private suspend fun Readable.canReadWholeBlob() = @Suppress("SameParameterValue") private suspend fun Readable.containsJsonKeys( vararg keys: String -): Try { - val json = readAsJson() - .getOrElse { return Try.failure(it) } +): Try { + val json = readDecodeOrElse( + decode = { it.decodeJson() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { return Try.success(false) } + ) return Try.success(json.keys().asSequence().toSet().containsAll(keys.toList())) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt index e074fb33d7..a87a6b9858 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/content/ResourceContentExtractor.kt @@ -14,7 +14,8 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.readAsString +import org.readium.r2.shared.util.data.decodeString +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.tryRecover @@ -59,11 +60,13 @@ public class HtmlResourceContentExtractor : ResourceContentExtractor { override suspend fun extractText(resource: Resource): Try = withContext(Dispatchers.IO) { resource - .readAsString() + .read() + .getOrElse { return@withContext Try.failure(it) } + .decodeString() .tryRecover { when (it) { - is DecodeError.Reading -> - return@withContext Try.failure(it.cause) + is DecodeError.OutOfMemory -> + return@withContext Try.failure(ReadError.OutOfMemory(it.cause)) is DecodeError.Decoding -> Try.success("") } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index 2ea80042b8..a79dad7d9c 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -16,10 +16,9 @@ import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.data.CompositeContainer -import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.readAsRwpm -import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.data.decodeRwpm +import org.readium.r2.shared.util.data.readDecodeOrElse import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpContainer import org.readium.r2.shared.util.mediatype.FormatRegistry @@ -80,14 +79,11 @@ internal class ParserAssetFactory( private suspend fun createParserAssetForManifest( asset: ResourceAsset ): Try { - val manifest = asset.resource.readAsRwpm() - .mapFailure { - when (it) { - is DecodeError.Decoding -> ReadError.Decoding(it.cause) - is DecodeError.Reading -> it.cause - } - } - .getOrElse { return Try.failure(CreateError.Reading(it)) } + val manifest = asset.resource + .readDecodeOrElse( + decode = { it.decodeRwpm() }, + recover = { return Try.failure(CreateError.Reading(it)) } + ) val baseUrl = manifest.linkWithRel("self")?.href?.resolve() if (baseUrl == null) { diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt index 36998083d8..f5f46ace28 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt @@ -12,7 +12,8 @@ package org.readium.r2.streamer.extensions import java.io.File import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.readAsXml +import org.readium.r2.shared.util.data.decodeXml +import org.readium.r2.shared.util.data.flatDecode import org.readium.r2.shared.util.use import org.readium.r2.shared.util.xml.ElementNode @@ -22,7 +23,7 @@ internal suspend fun Container<*>.readAsXmlOrNull(path: String): ElementNode? = /** Returns the resource data as an XML Document at the given [url], or null. */ internal suspend fun Container<*>.readAsXmlOrNull(url: Url): ElementNode? = - get(url)?.use { it.readAsXml().getOrNull() } + get(url)?.use { resource -> resource.flatDecode { it.decodeXml() }.getOrNull() } internal fun Iterable.guessTitle(): String? { val firstEntry = firstOrNull() ?: return null diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 5e384b39f4..ff18fcbb32 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -20,7 +20,9 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.readAsXml +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.data.decodeXml +import org.readium.r2.shared.util.data.readDecodeOrElse import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType @@ -52,7 +54,7 @@ public class EpubParser( val opfPath = getRootFilePath(asset.container) .getOrElse { return Try.failure(it) } - val opfResource = asset.container.get(opfPath) + val opfResource = asset.container[opfPath] ?: return Try.failure( PublicationParser.Error.Reading( ReadError.Decoding( @@ -60,9 +62,12 @@ public class EpubParser( ) ) ) - val opfXmlDocument = opfResource - .use { it.decodeOrFail(opfPath) { readAsXml() } } - .getOrElse { return Try.failure(it) } + val opfXmlDocument = opfResource.use { resource -> + resource.readDecodeOrElse( + decode = { it.decodeXml() }, + recover = { return Try.failure(PublicationParser.Error.Reading(it)) } + ) + } val packageDocument = PackageDocument.parse(opfXmlDocument, opfPath) ?: return Try.failure( PublicationParser.Error.Reading( @@ -109,8 +114,7 @@ public class EpubParser( private suspend fun getRootFilePath(container: Container): Try { val containerXmlUrl = Url("META-INF/container.xml")!! - val containerXmlResource = container - .get(containerXmlUrl) + val containerXmlResource = container[containerXmlUrl] ?: return Try.failure( PublicationParser.Error.Reading( ReadError.Decoding("container.xml not found.") @@ -118,8 +122,10 @@ public class EpubParser( ) return containerXmlResource - .use { it.decodeOrFail(containerXmlUrl) { readAsXml() } } - .getOrElse { return Try.failure(it) } + .readDecodeOrElse( + decode = { it.decodeXml() }, + recover = { return Try.failure(PublicationParser.Error.Reading(it)) } + ) .getFirst("rootfiles", Namespaces.OPC) ?.getFirst("rootfile", Namespaces.OPC) ?.getAttr("full-path") @@ -190,25 +196,16 @@ public class EpubParser( ?.toMap().orEmpty() } - private suspend fun Resource.decodeOrFail( + public suspend inline fun Readable.readDecodeOrElse( url: Url, - decode: suspend Resource.() -> Try - ): Try { - return decode() - .mapFailure { - when (it) { - is DecodeError.Reading -> - PublicationParser.Error.Reading(it.cause) - is DecodeError.Decoding -> - PublicationParser.Error.Reading( - ReadError.Decoding( - DebugError( - "Couldn't decode resource at $url", - it.cause - ) - ) - ) - } - } - } + decode: (value: ByteArray) -> Try, + recover: (ReadError) -> R + ): R = + readDecodeOrElse(decode, recover) { + recover( + ReadError.Decoding( + DebugError("Couldn't decode resource at $url", it.cause) + ) + ) + } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index fe8ed6e63b..af97d92e6e 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -17,10 +17,9 @@ import org.readium.r2.shared.publication.services.positionsServiceFactory import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.readAsRwpm -import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.data.decodeRwpm +import org.readium.r2.shared.util.data.readDecodeOrElse import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType @@ -55,26 +54,10 @@ public class ReadiumWebPubParser( ) val manifest = manifestResource - .readAsRwpm() - .getOrElse { - when (it) { - is DecodeError.Reading -> - return Try.failure( - PublicationParser.Error.Reading( - ReadError.Decoding(it.cause) - ) - ) - - is DecodeError.Decoding -> - return Try.failure( - PublicationParser.Error.Reading( - ReadError.Decoding( - DebugError("Failed to parse the RWPM Manifest.") - ) - ) - ) - } - } + .readDecodeOrElse( + decode = { it.decodeRwpm() }, + recover = { return Try.failure(PublicationParser.Error.Reading(it)) } + ) // Checks the requirements from the LCPDF specification. // https://readium.org/lcp-specs/notes/lcp-for-pdf.html From 14a14561b67da1b35fa5e4efe3b0c008322d718b Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Fri, 8 Dec 2023 01:29:34 +0100 Subject: [PATCH 62/86] Clean up decoding --- .../protection/ContentProtection.kt | 2 +- .../readium/r2/shared/util/data/Container.kt | 12 +++++ .../readium/r2/shared/util/data/Decoding.kt | 52 +++++-------------- .../readium/r2/shared/util/data/Reading.kt | 8 +++ .../r2/streamer/extensions/Container.kt | 13 ----- .../r2/streamer/parser/epub/EpubParser.kt | 25 ++++++--- 6 files changed, 52 insertions(+), 60 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index 1847e83eea..2f3570d34e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -59,7 +59,7 @@ public interface ContentProtection { /** * Attempts to unlock a potentially protected publication asset. * - * @return A [Asset] in case of success or a [Publication.OpenError] if the + * @return A [Asset] in case of success or a [OpenError] if the * asset can't be successfully opened even in restricted mode. */ public suspend fun open( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index 1188b9586f..911744f739 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -6,10 +6,13 @@ package org.readium.r2.shared.util.data +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.SuspendingCloseable +import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.use /** * A container provides access to a list of [Readable] entries. @@ -76,3 +79,12 @@ public class CompositeContainer( containers.forEach { it.close() } } } + +@InternalReadiumApi +public suspend inline fun Container<*>.readDecodeOrNull( + url: Url, + decode: (ByteArray) -> Try +): S? = + get(url)?.use { resource -> + resource.readDecodeOrNull(decode) + } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index 5acc6eb10d..815c236124 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -121,27 +121,7 @@ public suspend fun ByteArray.decodeBitmap(): Try = { DebugError("Could not decode content as a bitmap.") } ) -@InternalReadiumApi -public suspend inline fun ByteArray.flatDecode( - decode: (value: ByteArray) -> Try -): Try = - decode(this) - .tryRecover { error -> - when (error) { - is DecodeError.OutOfMemory -> - Try.failure(ReadError.OutOfMemory(error.cause)) - - is DecodeError.Decoding -> - Try.failure(ReadError.Decoding(error.cause)) - } - } - -@InternalReadiumApi -public suspend inline fun Readable.flatDecode( - decode: (value: ByteArray) -> Try -): Try = - read().flatMap { it.flatDecode(decode) } - +@Suppress("RedundantSuspendModifier") @InternalReadiumApi public suspend inline fun Try.decodeOrElse( decode: (value: ByteArray) -> Try, @@ -159,26 +139,12 @@ public suspend inline fun Try.decodeOrElse( } } +@Suppress("RedundantSuspendModifier") @InternalReadiumApi -public suspend inline fun ByteArray.decodeOrElse( - decode: (value: ByteArray) -> Try, - recover: (DecodeError.Decoding) -> R -): Try = - decode(this) - .tryRecover { error -> - when (error) { - is DecodeError.OutOfMemory -> - Try.failure(ReadError.OutOfMemory(error.cause)) - is DecodeError.Decoding -> - Try.success(recover(error)) - } - } - -@InternalReadiumApi -public suspend inline fun Readable.readOrElse( - recover: (ReadError) -> ByteArray -): ByteArray = - read().getOrElse(recover) +public suspend inline fun Try.decodeOrNull( + decode: (value: ByteArray) -> Try +): R? = + flatMap { decode(it) }.getOrNull() @InternalReadiumApi public suspend inline fun Readable.readDecodeOrElse( @@ -194,3 +160,9 @@ public suspend inline fun Readable.readDecodeOrElse( recover: (ReadError) -> R ): R = readDecodeOrElse(decode, recover) { recover(ReadError.Decoding(it)) } + +@InternalReadiumApi +public suspend inline fun Readable.readDecodeOrNull( + decode: (value: ByteArray) -> Try +): R? = + read().decodeOrNull(decode) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt index 5245eb46f3..e5f0535bac 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt @@ -7,12 +7,14 @@ package org.readium.r2.shared.util.data import java.io.IOException +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.getOrElse /** * Acts as a proxy to an actual data source by handling read access. @@ -122,3 +124,9 @@ private class BorrowedReadable( // Do nothing } } + +@InternalReadiumApi +public suspend inline fun Readable.readOrElse( + recover: (ReadError) -> ByteArray +): ByteArray = + read().getOrElse(recover) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt index f5f46ace28..00cdacf034 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt @@ -11,19 +11,6 @@ package org.readium.r2.streamer.extensions import java.io.File import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.decodeXml -import org.readium.r2.shared.util.data.flatDecode -import org.readium.r2.shared.util.use -import org.readium.r2.shared.util.xml.ElementNode - -/** Returns the resource data as an XML Document at the given [path], or null. */ -internal suspend fun Container<*>.readAsXmlOrNull(path: String): ElementNode? = - Url.fromDecodedPath(path)?.let { readAsXmlOrNull(it) } - -/** Returns the resource data as an XML Document at the given [url], or null. */ -internal suspend fun Container<*>.readAsXmlOrNull(url: Url): ElementNode? = - get(url)?.use { resource -> resource.flatDecode { it.decodeXml() }.getOrNull() } internal fun Iterable.guessTitle(): String? { val firstEntry = firstOrNull() ?: return null diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index ff18fcbb32..8c80c8c93c 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -23,13 +23,14 @@ import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.data.decodeXml import org.readium.r2.shared.util.data.readDecodeOrElse +import org.readium.r2.shared.util.data.readDecodeOrNull import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.TransformingContainer import org.readium.r2.shared.util.use -import org.readium.r2.streamer.extensions.readAsXmlOrNull +import org.readium.r2.shared.util.xml.ElementNode import org.readium.r2.streamer.parser.PublicationParser import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref @@ -139,7 +140,7 @@ public class EpubParser( } private suspend fun parseEncryptionData(container: Container): Map = - container.readAsXmlOrNull("META-INF/encryption.xml") + container.readDecodeXmlOrNull(path = "META-INF/encryption.xml") ?.let { EncryptionParser.parse(it) } ?: emptyMap() @@ -158,7 +159,7 @@ public class EpubParser( packageDocument.manifest .firstOrNull { it.properties.contains(Vocabularies.ITEM + "nav") } ?.let { navItem -> - container.readAsXmlOrNull(navItem.href) + container.readDecodeXmlOrNull(navItem.href) ?.let { NavigationDocumentParser.parse(it, navItem.href) } } ?.takeUnless { it.isEmpty() } @@ -176,15 +177,16 @@ public class EpubParser( return ncxItem ?.let { item -> - container.readAsXmlOrNull(item.href)?.let { NcxParser.parse(it, item.href) } + container.readDecodeXmlOrNull(item.href) + ?.let { NcxParser.parse(it, item.href) } } ?.takeUnless { it.isEmpty() } } private suspend fun parseDisplayOptions(container: Container): Map { val displayOptionsXml = - container.readAsXmlOrNull("META-INF/com.apple.ibooks.display-options.xml") - ?: container.readAsXmlOrNull("META-INF/com.kobobooks.display-options.xml") + container.readDecodeXmlOrNull("META-INF/com.apple.ibooks.display-options.xml") + ?: container.readDecodeXmlOrNull("META-INF/com.kobobooks.display-options.xml") return displayOptionsXml?.getFirst("platform", "") ?.get("option", "") @@ -208,4 +210,15 @@ public class EpubParser( ) ) } + + private suspend inline fun Container<*>.readDecodeXmlOrNull( + path: String + ): ElementNode? = + Url.fromDecodedPath(path)?.let { url -> readDecodeXmlOrNull(url) } + + /** Returns the resource data as an XML Document at the given [url], or null. */ + private suspend inline fun Container<*>.readDecodeXmlOrNull( + url: Url + ): ElementNode? = + readDecodeOrNull(url) { it.decodeXml() } } From 565df3858b9d3fb4861b1a0bcfe0cdc7de786d0b Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Fri, 8 Dec 2023 01:46:32 +0100 Subject: [PATCH 63/86] Cosmetic changes --- .../main/java/org/readium/r2/shared/util/data/Container.kt | 2 +- .../r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt | 2 +- .../java/org/readium/r2/streamer/parser/epub/EpubParser.kt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index 911744f739..503229fe2f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -81,7 +81,7 @@ public class CompositeContainer( } @InternalReadiumApi -public suspend inline fun Container<*>.readDecodeOrNull( +public suspend inline fun Container.readDecodeOrNull( url: Url, decode: (ByteArray) -> Try ): S? = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt index b727de0cc2..99c4b368f0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt @@ -45,6 +45,6 @@ public class DefaultMediaTypeSniffer : MediaTypeSniffer { override suspend fun sniffBlob(source: Readable): Try = sniffer.sniffBlob(source) - override suspend fun sniffContainer(container: Container<*>): Try = + override suspend fun sniffContainer(container: Container): Try = sniffer.sniffContainer(container) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 8c80c8c93c..6e6f9a596b 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -211,13 +211,13 @@ public class EpubParser( ) } - private suspend inline fun Container<*>.readDecodeXmlOrNull( + private suspend inline fun Container.readDecodeXmlOrNull( path: String ): ElementNode? = Url.fromDecodedPath(path)?.let { url -> readDecodeXmlOrNull(url) } /** Returns the resource data as an XML Document at the given [url], or null. */ - private suspend inline fun Container<*>.readDecodeXmlOrNull( + private suspend inline fun Container.readDecodeXmlOrNull( url: Url ): ElementNode? = readDecodeOrNull(url) { it.decodeXml() } From 379bac00b5853b5d1d4bf0e692c1c8ffe9106841 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Fri, 8 Dec 2023 04:31:52 +0100 Subject: [PATCH 64/86] Refactor user errors in testapp --- .../org/readium/r2/testapp/MainActivity.kt | 4 +- .../r2/testapp/bookshelf/BookshelfFragment.kt | 4 +- .../readium/r2/testapp/domain/ImportError.kt | 10 + .../r2/testapp/domain/ImportUserError.kt | 61 --- .../readium/r2/testapp/domain/LcpUserError.kt | 399 +++++------------- .../r2/testapp/domain/PublicationError.kt | 20 + .../r2/testapp/domain/PublicationUserError.kt | 68 --- .../r2/testapp/domain/ReadUserError.kt | 119 ++---- .../r2/testapp/drm/DrmManagementFragment.kt | 16 +- .../r2/testapp/drm/DrmManagementViewModel.kt | 16 +- .../r2/testapp/drm/LcpManagementViewModel.kt | 20 +- .../r2/testapp/reader/BaseReaderFragment.kt | 1 - .../readium/r2/testapp/reader/OpeningError.kt | 12 + .../r2/testapp/reader/OpeningUserError.kt | 49 --- .../r2/testapp/reader/ReaderActivity.kt | 1 - .../r2/testapp/reader/ReaderViewModel.kt | 19 +- .../r2/testapp/reader/VisualReaderFragment.kt | 3 +- .../readium/r2/testapp/reader/tts/TtsError.kt | 10 + .../r2/testapp/reader/tts/TtsUserError.kt | 60 --- .../r2/testapp/search/SearchUserError.kt | 32 +- .../org/readium/r2/testapp/utils/UserError.kt | 49 ++- 21 files changed, 262 insertions(+), 711 deletions(-) delete mode 100644 test-app/src/main/java/org/readium/r2/testapp/domain/ImportUserError.kt delete mode 100644 test-app/src/main/java/org/readium/r2/testapp/domain/PublicationUserError.kt delete mode 100644 test-app/src/main/java/org/readium/r2/testapp/reader/OpeningUserError.kt delete mode 100644 test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsUserError.kt diff --git a/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt b/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt index 81a2a5c70f..eab4b57c6c 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt @@ -17,8 +17,6 @@ import androidx.navigation.ui.setupWithNavController import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar import org.readium.r2.shared.util.toDebugDescription -import org.readium.r2.testapp.domain.ImportUserError -import org.readium.r2.testapp.utils.getUserMessage import timber.log.Timber class MainActivity : AppCompatActivity() { @@ -64,7 +62,7 @@ class MainActivity : AppCompatActivity() { is MainViewModel.Event.ImportPublicationError -> { Timber.e(event.error.toDebugDescription()) - ImportUserError(event.error).getUserMessage(this) + event.error.toUserError().getUserMessage(this) } } Snackbar.make( diff --git a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt index 63c2715028..22d16e6dae 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/bookshelf/BookshelfFragment.kt @@ -29,9 +29,7 @@ import org.readium.r2.testapp.R import org.readium.r2.testapp.data.model.Book import org.readium.r2.testapp.databinding.FragmentBookshelfBinding import org.readium.r2.testapp.opds.GridAutoFitLayoutManager -import org.readium.r2.testapp.reader.OpeningUserError import org.readium.r2.testapp.reader.ReaderActivityContract -import org.readium.r2.testapp.utils.getUserMessage import org.readium.r2.testapp.utils.viewLifecycle import timber.log.Timber @@ -160,7 +158,7 @@ class BookshelfFragment : Fragment() { when (event) { is BookshelfViewModel.Event.OpenPublicationError -> { Timber.e(event.error.toDebugDescription()) - OpeningUserError(event.error).getUserMessage(requireContext()) + (event.error).toUserError().getUserMessage(requireContext()) } is BookshelfViewModel.Event.LaunchReader -> { diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt index 142079cd21..0cfc1abbc9 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt @@ -9,6 +9,8 @@ package org.readium.r2.testapp.domain import org.readium.r2.lcp.LcpError import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError sealed class ImportError( override val cause: Error? @@ -34,4 +36,12 @@ sealed class ImportError( class DatabaseError(override val cause: Error) : ImportError(cause) + + fun toUserError(): UserError = when (this) { + is DatabaseError -> UserError(R.string.import_publication_unable_add_pub_database) + is DownloadFailed -> UserError(R.string.import_publication_download_failed) + is LcpAcquisitionFailed -> cause.toUserError() + is OpdsError -> UserError(R.string.import_publication_no_acquisition) + is PublicationError -> cause.toUserError() + } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportUserError.kt deleted file mode 100644 index 0780b0c32e..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportUserError.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.testapp.domain - -import androidx.annotation.StringRes -import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.downloads.DownloadManager -import org.readium.r2.testapp.R -import org.readium.r2.testapp.utils.UserError - -sealed class ImportUserError( - override val content: UserError.Content, - override val cause: UserError? -) : UserError { - - constructor(@StringRes userMessageId: Int) : - this(UserError.Content(userMessageId), null) - - constructor(cause: UserError) : - this(UserError.Content(cause), cause) - - class LcpAcquisitionFailed( - override val cause: LcpUserError - ) : ImportUserError(cause) - - class PublicationError( - override val cause: PublicationUserError - ) : ImportUserError(cause) - - class DownloadFailed( - val error: DownloadManager.DownloadError - ) : ImportUserError(R.string.import_publication_download_failed) - - class OpdsError( - val error: Error - ) : ImportUserError(R.string.import_publication_no_acquisition) - - class DatabaseError : - ImportUserError(R.string.import_publication_unable_add_pub_database) - - companion object { - - operator fun invoke(error: ImportError): ImportUserError = - when (error) { - is ImportError.DatabaseError -> - DatabaseError() - is ImportError.DownloadFailed -> - DownloadFailed(error.cause) - is ImportError.LcpAcquisitionFailed -> - LcpAcquisitionFailed(LcpUserError(error.cause)) - is ImportError.OpdsError -> - OpdsError(error.cause) - is ImportError.PublicationError -> - PublicationError(PublicationUserError(error.cause)) - } - } -} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/LcpUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/LcpUserError.kt index 0a4de090a1..b7b3fb519c 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/LcpUserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/LcpUserError.kt @@ -6,308 +6,107 @@ package org.readium.r2.testapp.domain -import androidx.annotation.PluralsRes -import androidx.annotation.StringRes -import java.util.Date import org.readium.r2.lcp.LcpError -import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.Url import org.readium.r2.testapp.R import org.readium.r2.testapp.utils.UserError -sealed class LcpUserError( - override val content: UserError.Content, - override val cause: UserError? = null -) : UserError { - - constructor(userMessageId: Int, vararg args: Any, quantity: Int? = null) : - this(UserError.Content(userMessageId, quantity, args)) - - constructor(@StringRes userMessageId: Int, vararg args: Any) : - this(UserError.Content(userMessageId, *args)) - - /** The interaction is not available with this License. */ - object LicenseInteractionNotAvailable : LcpUserError( - R.string.lcp_error_license_interaction_not_available - ) - - /** This License's profile is not supported by liblcp. */ - object LicenseProfileNotSupported : LcpUserError( - R.string.lcp_error_license_profile_not_supported - ) - - /** Failed to retrieve the Certificate Revocation List. */ - object CrlFetching : LcpUserError(R.string.lcp_error_crl_fetching) - - /** A network request failed with the given exception. */ - class Network(val error: Error?) : - LcpUserError(R.string.lcp_error_network) - - /** - * An unexpected LCP exception occurred. Please post an issue on r2-lcp-kotlin with the error - * message and how to reproduce it. - */ - class Runtime(val message: String) : - LcpUserError(R.string.lcp_error_runtime) - - /** An unknown low-level exception was reported. */ - class Unknown(val error: Error?) : - LcpUserError(R.string.lcp_error_unknown) - - /** - * Errors while checking the status of the License, using the Status Document. - * - * The app should notify the user and stop there. The message to the user must be clear about - * the status of the license: don't display "expired" if the status is "revoked". The date and - * time corresponding to the new status should be displayed (e.g. "The license expired on 01 - * January 2018"). - */ - sealed class LicenseStatus(userMessageId: Int, vararg args: Any, quantity: Int? = null) : - LcpUserError(userMessageId, args, quantity = quantity) { - - constructor(@StringRes userMessageId: Int, vararg args: Any) : - this(userMessageId, *args, quantity = null) - - constructor(@PluralsRes userMessageId: Int, quantity: Int, vararg args: Any) : - this(userMessageId, *args, quantity = quantity) - - class Cancelled(val date: Date) : - LicenseStatus(R.string.lcp_error_license_status_cancelled, date) - - class Returned(val date: Date) : - LicenseStatus(R.string.lcp_error_license_status_returned, date) - - class NotStarted(val start: Date) : - LicenseStatus(R.string.lcp_error_license_status_not_started, start) - - class Expired(val end: Date) : - LicenseStatus(R.string.lcp_error_license_status_expired, end) - - /** - * If the license has been revoked, the user message should display the number of devices which - * registered to the server. This count can be calculated from the number of "register" events - * in the status document. If no event is logged in the status document, no such message should - * appear (certainly not "The license was registered by 0 devices"). - */ - class Revoked(val date: Date, val devicesCount: Int) : - LicenseStatus( - R.plurals.lcp_error_license_status_revoked, - devicesCount, - date, - devicesCount - ) - } - - /** - * Errors while renewing a loan. - */ - sealed class Renew(@StringRes userMessageId: Int) : LcpUserError(userMessageId) { - - /** Your publication could not be renewed properly. */ - object RenewFailed : Renew(R.string.lcp_error_renew_renew_failed) - - /** Incorrect renewal period, your publication could not be renewed. */ - class InvalidRenewalPeriod(val maxRenewDate: Date?) : - Renew(R.string.lcp_error_renew_invalid_renewal_period) - - /** An unexpected error has occurred on the licensing server. */ - object UnexpectedServerError : - Renew(R.string.lcp_error_renew_unexpected_server_error) - } - - /** - * Errors while returning a loan. - */ - sealed class Return(@StringRes userMessageId: Int) : LcpUserError(userMessageId) { - - /** Your publication could not be returned properly. */ - object ReturnFailed : Return(R.string.lcp_error_return_return_failed) - - /** Your publication has already been returned before or is expired. */ - - object AlreadyReturnedOrExpired : - Return(R.string.lcp_error_return_already_returned_or_expired) - - /** An unexpected error has occurred on the licensing server. */ - object UnexpectedServerError : - Return(R.string.lcp_error_return_unexpected_server_error) - } - - /** - * Errors while parsing the License or Status JSON Documents. - */ - sealed class Parsing( - @StringRes userMessageId: Int = R.string.lcp_error_parsing - ) : LcpUserError(userMessageId) { - - /** The JSON is malformed and can't be parsed. */ - object MalformedJSON : Parsing(R.string.lcp_error_parsing_malformed_json) - - /** The JSON is not representing a valid License Document. */ - object LicenseDocument : Parsing(R.string.lcp_error_parsing_license_document) - - /** The JSON is not representing a valid Status Document. */ - object StatusDocument : Parsing(R.string.lcp_error_parsing_status_document) - - /** Invalid Link. */ - object Link : Parsing() - - /** Invalid Encryption. */ - object Encryption : Parsing() - - /** Invalid License Document Signature. */ - object Signature : Parsing() - - /** Invalid URL for link with [rel]. */ - class Url(val rel: String) : Parsing() - } - - /** - * Errors while reading or writing a LCP container (LCPL, EPUB, LCPDF, etc.) - */ - sealed class Container(@StringRes userMessageId: Int) : LcpUserError(userMessageId) { - - /** Can't access the container, it's format is wrong. */ - object OpenFailed : Container(R.string.lcp_error_container_open_failed) - - /** The file at given relative path is not found in the Container. */ - class FileNotFound(val url: Url) : Container(R.string.lcp_error_container_file_not_found) - - /** Can't read the file at given relative path in the Container. */ - class ReadFailed(val url: Url?) : Container(R.string.lcp_error_container_read_failed) - - /** Can't write the file at given relative path in the Container. */ - class WriteFailed(val url: Url?) : Container(R.string.lcp_error_container_write_failed) - } - - /** - * An error occurred while checking the integrity of the License, it can't be retrieved. - */ - sealed class LicenseIntegrity(@StringRes userMessageId: Int) : LcpUserError( - userMessageId - ) { - - object CertificateRevoked : - LicenseIntegrity(R.string.lcp_error_license_integrity_certificate_revoked) - - object InvalidCertificateSignature : - LicenseIntegrity(R.string.lcp_error_license_integrity_invalid_certificate_signature) - - object InvalidLicenseSignatureDate : - LicenseIntegrity(R.string.lcp_error_license_integrity_invalid_license_signature_date) - - object InvalidLicenseSignature : - LicenseIntegrity(R.string.lcp_error_license_integrity_invalid_license_signature) - - object InvalidUserKeyCheck : - LicenseIntegrity(R.string.lcp_error_license_integrity_invalid_user_key_check) - } - - sealed class Decryption(@StringRes userMessageId: Int) : LcpUserError(userMessageId) { - - object ContentKeyDecryptError : - Decryption(R.string.lcp_error_decryption_content_key_decrypt_error) - - object ContentDecryptError : - Decryption(R.string.lcp_error_decryption_content_decrypt_error) - } - - companion object { - - operator fun invoke(error: LcpError): LcpUserError = - when (error) { - is LcpError.Container -> - when (error) { - is LcpError.Container.FileNotFound -> - Container.FileNotFound(error.url) - LcpError.Container.OpenFailed -> - Container.OpenFailed - is LcpError.Container.ReadFailed -> - Container.ReadFailed(error.url) - is LcpError.Container.WriteFailed -> - Container.WriteFailed(error.url) - } - LcpError.CrlFetching -> - CrlFetching - is LcpError.Decryption -> - when (error) { - LcpError.Decryption.ContentDecryptError -> - Decryption.ContentDecryptError - LcpError.Decryption.ContentKeyDecryptError -> - Decryption.ContentKeyDecryptError - } - is LcpError.LicenseIntegrity -> - when (error) { - LcpError.LicenseIntegrity.CertificateRevoked -> - LicenseIntegrity.CertificateRevoked - LcpError.LicenseIntegrity.InvalidCertificateSignature -> - LicenseIntegrity.InvalidCertificateSignature - LcpError.LicenseIntegrity.InvalidLicenseSignature -> - LicenseIntegrity.InvalidLicenseSignature - LcpError.LicenseIntegrity.InvalidLicenseSignatureDate -> - LicenseIntegrity.InvalidLicenseSignatureDate - LcpError.LicenseIntegrity.InvalidUserKeyCheck -> - LicenseIntegrity.InvalidUserKeyCheck - } - LcpError.LicenseInteractionNotAvailable -> - LicenseInteractionNotAvailable - LcpError.LicenseProfileNotSupported -> - LicenseProfileNotSupported - is LcpError.LicenseStatus -> - when (error) { - is LcpError.LicenseStatus.Cancelled -> - LicenseStatus.Cancelled(error.date) - is LcpError.LicenseStatus.Expired -> - LicenseStatus.Expired(error.end) - is LcpError.LicenseStatus.NotStarted -> - LicenseStatus.NotStarted(error.start) - is LcpError.LicenseStatus.Returned -> - LicenseStatus.Returned(error.date) - is LcpError.LicenseStatus.Revoked -> - LicenseStatus.Revoked(error.date, error.devicesCount) - } - is LcpError.Network -> - Network(error.cause) - is LcpError.Parsing -> - when (error) { - LcpError.Parsing.Encryption -> - Parsing.Encryption - LcpError.Parsing.LicenseDocument -> - Parsing.LicenseDocument - LcpError.Parsing.Link -> - Parsing.Link - LcpError.Parsing.MalformedJSON -> - Parsing.MalformedJSON - LcpError.Parsing.Signature -> - Parsing.Signature - LcpError.Parsing.StatusDocument -> - Parsing.StatusDocument - is LcpError.Parsing.Url -> - Parsing.Url(error.rel) - } - - is LcpError.Renew -> - when (error) { - is LcpError.Renew.InvalidRenewalPeriod -> - Renew.InvalidRenewalPeriod(error.maxRenewDate) - LcpError.Renew.RenewFailed -> - Renew.RenewFailed - LcpError.Renew.UnexpectedServerError -> - Renew.UnexpectedServerError - } - is LcpError.Return -> - when (error) { - LcpError.Return.AlreadyReturnedOrExpired -> - Return.AlreadyReturnedOrExpired - LcpError.Return.ReturnFailed -> - Return.ReturnFailed - LcpError.Return.UnexpectedServerError -> - Return.UnexpectedServerError - } - is LcpError.Runtime -> - Runtime(error.message) - is LcpError.Unknown -> - Unknown(error.cause) - } - } +fun LcpError.toUserError(): UserError = when (this) { + LcpError.LicenseInteractionNotAvailable -> + UserError(R.string.lcp_error_license_interaction_not_available) + LcpError.LicenseProfileNotSupported -> + UserError(R.string.lcp_error_license_profile_not_supported) + LcpError.CrlFetching -> + UserError(R.string.lcp_error_crl_fetching) + is LcpError.Network -> + UserError(R.string.lcp_error_network) + + is LcpError.Runtime -> + UserError(R.string.lcp_error_runtime) + is LcpError.Unknown -> + UserError(R.string.lcp_error_unknown) + + is LcpError.Container -> + when (this) { + is LcpError.Container.FileNotFound -> + UserError(R.string.lcp_error_container_file_not_found) + LcpError.Container.OpenFailed -> + UserError(R.string.lcp_error_container_open_failed) + is LcpError.Container.ReadFailed -> + UserError(R.string.lcp_error_container_read_failed) + is LcpError.Container.WriteFailed -> + UserError(R.string.lcp_error_container_write_failed) + } + + is LcpError.Decryption -> + when (this) { + LcpError.Decryption.ContentDecryptError -> + UserError(R.string.lcp_error_decryption_content_decrypt_error) + LcpError.Decryption.ContentKeyDecryptError -> + UserError(R.string.lcp_error_decryption_content_key_decrypt_error) + } + + is LcpError.LicenseIntegrity -> + when (this) { + LcpError.LicenseIntegrity.CertificateRevoked -> + UserError(R.string.lcp_error_license_integrity_certificate_revoked) + LcpError.LicenseIntegrity.InvalidCertificateSignature -> + UserError(R.string.lcp_error_license_integrity_invalid_certificate_signature) + LcpError.LicenseIntegrity.InvalidLicenseSignature -> + UserError(R.string.lcp_error_license_integrity_invalid_license_signature) + LcpError.LicenseIntegrity.InvalidLicenseSignatureDate -> + UserError(R.string.lcp_error_license_integrity_invalid_license_signature_date) + LcpError.LicenseIntegrity.InvalidUserKeyCheck -> + UserError(R.string.lcp_error_license_integrity_invalid_user_key_check) + } + + is LcpError.LicenseStatus -> + when (this) { + is LcpError.LicenseStatus.Cancelled -> + UserError(R.string.lcp_error_license_status_cancelled, date) + is LcpError.LicenseStatus.Expired -> + UserError(R.string.lcp_error_license_status_expired, end) + is LcpError.LicenseStatus.NotStarted -> + UserError(R.string.lcp_error_license_status_not_started, start) + is LcpError.LicenseStatus.Returned -> + UserError(R.string.lcp_error_license_status_returned, date) + is LcpError.LicenseStatus.Revoked -> + UserError( + R.plurals.lcp_error_license_status_revoked, + devicesCount, + date, + devicesCount + ) + } + + is LcpError.Parsing -> + when (this) { + LcpError.Parsing.LicenseDocument -> + UserError(R.string.lcp_error_parsing_license_document) + LcpError.Parsing.MalformedJSON -> + UserError(R.string.lcp_error_parsing_malformed_json) + LcpError.Parsing.StatusDocument -> + UserError(R.string.lcp_error_parsing_license_document) + else -> + UserError(R.string.lcp_error_parsing) + } + + is LcpError.Renew -> + when (this) { + is LcpError.Renew.InvalidRenewalPeriod -> + UserError(R.string.lcp_error_renew_invalid_renewal_period) + LcpError.Renew.RenewFailed -> + UserError(R.string.lcp_error_renew_renew_failed) + LcpError.Renew.UnexpectedServerError -> + UserError(R.string.lcp_error_renew_unexpected_server_error) + } + + is LcpError.Return -> + when (this) { + LcpError.Return.AlreadyReturnedOrExpired -> + UserError(R.string.lcp_error_return_already_returned_or_expired) + LcpError.Return.ReturnFailed -> + UserError(R.string.lcp_error_return_return_failed) + LcpError.Return.UnexpectedServerError -> + UserError(R.string.lcp_error_return_unexpected_server_error) + } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index 0560aee340..e1c1553515 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -10,6 +10,8 @@ import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetri import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.streamer.PublicationFactory +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError sealed class PublicationError( override val message: String, @@ -36,6 +38,24 @@ sealed class PublicationError( class Unexpected(cause: Error) : PublicationError(cause.message, cause.cause) + fun toUserError(): UserError = + when (this) { + is InvalidPublication -> + UserError(R.string.publication_error_invalid_publication) + is Unexpected -> + UserError(R.string.publication_error_unexpected) + is UnsupportedArchiveFormat -> + UserError(R.string.publication_error_unsupported_archive) + is UnsupportedContentProtection -> + UserError(R.string.publication_error_unsupported_protection) + is UnsupportedPublication -> + UserError(R.string.publication_error_unsupported_asset) + is UnsupportedScheme -> + UserError(R.string.publication_error_scheme_not_supported) + is ReadError -> + cause.toUserError() + } + companion object { operator fun invoke(error: AssetRetriever.RetrieveError): PublicationError = diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationUserError.kt deleted file mode 100644 index 295e6a1636..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationUserError.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.testapp.domain - -import androidx.annotation.StringRes -import org.readium.r2.shared.util.Error -import org.readium.r2.testapp.R -import org.readium.r2.testapp.utils.UserError - -sealed class PublicationUserError( - override val content: UserError.Content, - override val cause: UserError? = null -) : UserError { - - constructor(@StringRes userMessageId: Int) : - this(UserError.Content(userMessageId), null) - - class ReadError(override val cause: ReadUserError) : - PublicationUserError(cause.content, cause.cause) - - class UnsupportedScheme(val error: Error) : - PublicationUserError(R.string.publication_error_scheme_not_supported) - - class UnsupportedContentProtection(val error: Error? = null) : - PublicationUserError(R.string.publication_error_unsupported_protection) - class UnsupportedArchiveFormat(val error: Error) : - PublicationUserError(R.string.publication_error_unsupported_archive) - - class UnsupportedPublication(val error: Error? = null) : - PublicationUserError(R.string.publication_error_unsupported_asset) - - class InvalidPublication(val error: Error) : - PublicationUserError(R.string.publication_error_invalid_publication) - - class Unexpected(val error: Error) : - PublicationUserError(R.string.publication_error_unexpected) - - companion object { - - operator fun invoke(error: PublicationError): PublicationUserError = - when (error) { - is PublicationError.InvalidPublication -> - InvalidPublication(error) - - is PublicationError.Unexpected -> - Unexpected(error) - - is PublicationError.UnsupportedArchiveFormat -> - UnsupportedArchiveFormat(error) - - is PublicationError.UnsupportedContentProtection -> - UnsupportedContentProtection(error) - - is PublicationError.UnsupportedPublication -> - UnsupportedPublication(error) - - is PublicationError.UnsupportedScheme -> - UnsupportedScheme(error) - - is PublicationError.ReadError -> - ReadError(ReadUserError(error.cause)) - } - } -} diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt index 7ef72edd52..048a80ba46 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt @@ -6,8 +6,6 @@ package org.readium.r2.testapp.domain -import androidx.annotation.StringRes -import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.content.ContentResolverError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.file.FileSystemError @@ -16,88 +14,43 @@ import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.testapp.R import org.readium.r2.testapp.utils.UserError -sealed class ReadUserError( - override val content: UserError.Content, - override val cause: UserError? = null -) : UserError { - constructor(@StringRes userMessageId: Int) : - this(UserError.Content(userMessageId), null) - - class HttpNotFound(val error: Error) : - ReadUserError(R.string.publication_error_network_not_found) - - class HttpForbidden(val error: Error) : - ReadUserError(R.string.publication_error_network_forbidden) - - class HttpConnectivity(val error: Error) : - ReadUserError(R.string.publication_error_network_connectivity) - - class HttpUnexpected(val error: Error) : - ReadUserError(R.string.publication_error_network_unexpected) - - class FsNotFound(val error: Error) : - ReadUserError(R.string.publication_error_filesystem_not_found) - - class FsUnexpected(val error: Error) : - ReadUserError(R.string.publication_error_filesystem_unexpected) - - class OutOfMemory(val error: Error) : - ReadUserError(R.string.publication_error_out_of_memory) - - class InvalidPublication(val error: Error) : - ReadUserError(R.string.publication_error_invalid_publication) - - class Unexpected(val error: Error) : - ReadUserError(R.string.publication_error_unexpected) - - companion object { - - operator fun invoke(error: ReadError): ReadUserError = - when (error) { - is ReadError.Access -> - when (val cause = error.cause) { - is HttpError -> ReadUserError(cause) - is FileSystemError -> ReadUserError(cause) - is ContentResolverError -> ReadUserError(cause) - else -> Unexpected(cause) - } - is ReadError.Decoding -> InvalidPublication(error) - is ReadError.OutOfMemory -> OutOfMemory(error) - is ReadError.UnsupportedOperation -> Unexpected(error) - } +fun ReadError.toUserError(): UserError = when (this) { + is ReadError.Access -> + when (val cause = this.cause) { + is HttpError -> cause.toUserError() + is FileSystemError -> cause.toUserError() + is ContentResolverError -> cause.toUserError() + else -> UserError(R.string.error_unexpected) + } + + is ReadError.Decoding -> UserError(R.string.publication_error_invalid_publication) + is ReadError.OutOfMemory -> UserError(R.string.publication_error_out_of_memory) + is ReadError.UnsupportedOperation -> UserError(R.string.publication_error_unexpected) +} - private operator fun invoke(error: HttpError): ReadUserError = - when (error) { - is HttpError.IO -> - HttpUnexpected(error) - is HttpError.MalformedResponse -> - HttpUnexpected(error) - is HttpError.Redirection -> - HttpUnexpected(error) - is HttpError.Timeout -> - HttpConnectivity(error) - is HttpError.Unreachable -> - HttpConnectivity(error) - is HttpError.ErrorResponse -> - when (error.status) { - HttpStatus.Forbidden -> HttpForbidden(error) - HttpStatus.NotFound -> HttpNotFound(error) - else -> HttpUnexpected(error) - } - } +fun HttpError.toUserError(): UserError = when (this) { + is HttpError.IO -> UserError(R.string.publication_error_network_unexpected) + is HttpError.MalformedResponse -> UserError(R.string.publication_error_network_unexpected) + is HttpError.Redirection -> UserError(R.string.publication_error_network_unexpected) + is HttpError.Timeout -> UserError(R.string.publication_error_network_connectivity) + is HttpError.Unreachable -> UserError(R.string.publication_error_network_connectivity) + is HttpError.ErrorResponse -> when (status) { + HttpStatus.Forbidden -> UserError(R.string.publication_error_network_forbidden) + HttpStatus.NotFound -> UserError(R.string.publication_error_network_not_found) + else -> UserError(R.string.publication_error_network_unexpected) + } +} - private operator fun invoke(error: FileSystemError): ReadUserError = - when (error) { - is FileSystemError.Forbidden -> FsUnexpected(error) - is FileSystemError.IO -> FsUnexpected(error) - is FileSystemError.NotFound -> FsNotFound(error) - } +fun FileSystemError.toUserError(): UserError = when (this) { + is FileSystemError.Forbidden -> UserError(R.string.publication_error_filesystem_unexpected) + is FileSystemError.IO -> UserError(R.string.publication_error_filesystem_unexpected) + is FileSystemError.NotFound -> UserError(R.string.publication_error_filesystem_not_found) +} - private operator fun invoke(error: ContentResolverError): ReadUserError = - when (error) { - is ContentResolverError.FileNotFound -> FsNotFound(error) - is ContentResolverError.IO -> FsUnexpected(error) - is ContentResolverError.NotAvailable -> FsUnexpected(error) - } - } +fun ContentResolverError.toUserError(): UserError = when (this) { + is ContentResolverError.FileNotFound -> UserError( + R.string.publication_error_filesystem_not_found + ) + is ContentResolverError.IO -> UserError(R.string.publication_error_filesystem_unexpected) + is ContentResolverError.NotAvailable -> UserError(R.string.error_unexpected) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt index 060e79461c..e4cbe4309b 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementFragment.kt @@ -22,13 +22,10 @@ import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat import org.readium.r2.lcp.MaterialRenewListener import org.readium.r2.lcp.lcpLicense -import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.toDebugDescription import org.readium.r2.testapp.R import org.readium.r2.testapp.databinding.FragmentDrmManagementBinding import org.readium.r2.testapp.reader.ReaderViewModel -import org.readium.r2.testapp.utils.UserError -import org.readium.r2.testapp.utils.getUserMessage import org.readium.r2.testapp.utils.viewLifecycle import timber.log.Timber @@ -108,7 +105,7 @@ class DrmManagementFragment : Fragment() { .onSuccess { newDate -> binding.drmValueEnd.text = newDate.toFormattedString() }.onFailure { error -> - error.toastUserMessage(requireView()) + error.report(requireView()) } } } @@ -126,7 +123,7 @@ class DrmManagementFragment : Fragment() { val result = DrmManagementContract.createResult(hasReturned = true) setFragmentResult(DrmManagementContract.REQUEST_KEY, result) }.onFailure { exception -> - exception.toastUserMessage(requireView()) + exception.report(requireView()) } } } @@ -138,10 +135,7 @@ private fun Date?.toFormattedString() = DateTime(this).toString(DateTimeFormat.shortDateTime()).orEmpty() // FIXME: the toast is drawn behind the navigation bar -private fun Error.toastUserMessage(view: View) { - if (this is UserError) { - Snackbar.make(view, getUserMessage(view.context), Snackbar.LENGTH_LONG).show() - } - - Timber.w(toDebugDescription()) +private fun DrmManagementViewModel.DrmError.report(view: View) { + Snackbar.make(view, toUserError().getUserMessage(view.context), Snackbar.LENGTH_LONG).show() + Timber.w(error.toDebugDescription()) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt index 7ecab5c6a3..551f2682d1 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/drm/DrmManagementViewModel.kt @@ -8,13 +8,19 @@ package org.readium.r2.testapp.drm import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel -import java.util.* -import org.readium.r2.shared.util.DebugError +import java.util.Date import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try +import org.readium.r2.testapp.utils.UserError abstract class DrmManagementViewModel : ViewModel() { + interface DrmError { + + val error: Error + fun toUserError(): UserError + } + abstract val type: String open val state: String? = null @@ -35,11 +41,9 @@ abstract class DrmManagementViewModel : ViewModel() { open val canRenewLoan: Boolean = false - open suspend fun renewLoan(fragment: Fragment): Try = - Try.failure(DebugError("Renewing a loan is not supported")) + abstract suspend fun renewLoan(fragment: Fragment): Try open val canReturnPublication: Boolean = false - open suspend fun returnPublication(): Try = - Try.failure(DebugError("Returning a publication is not supported")) + abstract suspend fun returnPublication(): Try } diff --git a/test-app/src/main/java/org/readium/r2/testapp/drm/LcpManagementViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/drm/LcpManagementViewModel.kt index b4f4790591..e5939baa9a 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/drm/LcpManagementViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/drm/LcpManagementViewModel.kt @@ -9,10 +9,12 @@ package org.readium.r2.testapp.drm import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import java.util.* +import java.util.Date +import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpLicense -import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try +import org.readium.r2.testapp.domain.toUserError +import org.readium.r2.testapp.utils.UserError class LcpManagementViewModel( private val lcpLicense: LcpLicense, @@ -32,6 +34,14 @@ class LcpManagementViewModel( .newInstance(lcpLicense, renewListener) } + class LcpDrmError( + override val error: LcpError + ) : DrmError { + + override fun toUserError(): UserError = + error.toUserError() + } + override val type: String = "LCP" override val state: String? @@ -65,13 +75,15 @@ class LcpManagementViewModel( override val canRenewLoan: Boolean get() = lcpLicense.canRenewLoan - override suspend fun renewLoan(fragment: Fragment): Try { + override suspend fun renewLoan(fragment: Fragment): Try { return lcpLicense.renewLoan(renewListener) + .mapFailure { LcpDrmError(it) } } override val canReturnPublication: Boolean get() = lcpLicense.canReturnPublication - override suspend fun returnPublication(): Try = + override suspend fun returnPublication(): Try = lcpLicense.returnPublication() + .mapFailure { LcpDrmError(it) } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/BaseReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/BaseReaderFragment.kt index 355fd4205b..dd227388fc 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/BaseReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/BaseReaderFragment.kt @@ -25,7 +25,6 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.testapp.R import org.readium.r2.testapp.reader.preferences.UserPreferencesBottomSheetDialogFragment import org.readium.r2.testapp.utils.UserError -import org.readium.r2.testapp.utils.getUserMessage /* * Base reader fragment class diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt index 182d9c2871..24082e781c 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt @@ -7,6 +7,8 @@ package org.readium.r2.testapp.reader import org.readium.r2.shared.util.Error +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError sealed class OpeningError( override val cause: Error? @@ -26,4 +28,14 @@ sealed class OpeningError( class AudioEngineInitialization( cause: Error ) : OpeningError(cause) + + fun toUserError(): UserError = + when (this) { + is AudioEngineInitialization -> + UserError(R.string.opening_publication_audio_engine_initialization) + is PublicationError -> + cause.toUserError() + is RestrictedPublication -> + UserError(R.string.publication_error_restricted) + } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningUserError.kt deleted file mode 100644 index ec6cd3309c..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningUserError.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.testapp.reader - -import androidx.annotation.StringRes -import org.readium.r2.shared.util.Error -import org.readium.r2.testapp.R -import org.readium.r2.testapp.domain.PublicationUserError -import org.readium.r2.testapp.utils.UserError - -sealed class OpeningUserError( - override val content: UserError.Content, - override val cause: UserError? -) : UserError { - - constructor(@StringRes userMessageId: Int) : - this(UserError.Content(userMessageId), null) - - constructor(cause: UserError) : - this(UserError.Content(cause), cause) - - class PublicationError( - override val cause: PublicationUserError - ) : OpeningUserError(cause) - - class RestrictedPublication(val error: Error? = null) : - OpeningUserError(R.string.publication_error_restricted) - - class AudioEngineInitialization( - val error: Error - ) : OpeningUserError(R.string.opening_publication_audio_engine_initialization) - - companion object { - - operator fun invoke(error: OpeningError): OpeningUserError = - when (error) { - is OpeningError.AudioEngineInitialization -> - AudioEngineInitialization(error) - is OpeningError.PublicationError -> - PublicationError(PublicationUserError(error.cause)) - is OpeningError.RestrictedPublication -> - RestrictedPublication(error) - } - } -} diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivity.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivity.kt index 0045fee82a..831bd43ed7 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivity.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderActivity.kt @@ -28,7 +28,6 @@ import org.readium.r2.testapp.drm.DrmManagementFragment import org.readium.r2.testapp.outline.OutlineContract import org.readium.r2.testapp.outline.OutlineFragment import org.readium.r2.testapp.utils.UserError -import org.readium.r2.testapp.utils.getUserMessage import org.readium.r2.testapp.utils.launchWebBrowser /* diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt index 840347b5c7..babe3cbee4 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderViewModel.kt @@ -38,11 +38,10 @@ import org.readium.r2.testapp.Application import org.readium.r2.testapp.R import org.readium.r2.testapp.data.BookRepository import org.readium.r2.testapp.data.model.Highlight -import org.readium.r2.testapp.domain.ReadUserError +import org.readium.r2.testapp.domain.toUserError import org.readium.r2.testapp.reader.preferences.UserPreferencesViewModel import org.readium.r2.testapp.reader.tts.TtsViewModel import org.readium.r2.testapp.search.SearchPagingSource -import org.readium.r2.testapp.search.SearchUserError import org.readium.r2.testapp.utils.EventChannel import org.readium.r2.testapp.utils.UserError import org.readium.r2.testapp.utils.createViewModelFactory @@ -62,14 +61,6 @@ class ReaderViewModel( ImageNavigatorFragment.Listener, PdfNavigatorFragment.Listener { - class ReaderUserError( - override val cause: UserError - ) : UserError { - - override val content: UserError.Content = - UserError.Content(R.string.reader_error) - } - val readerInitData = try { checkNotNull(readerRepository[bookId]) @@ -223,7 +214,9 @@ class ReaderViewModel( searchIterator = publication.search(query) ?: run { activityChannel.send( - ActivityCommand.ToastError(SearchUserError.PublicationNotSearchable) + ActivityCommand.ToastError( + UserError(R.string.search_error_not_searchable) + ) ) null } @@ -272,9 +265,7 @@ class ReaderViewModel( override fun onResourceLoadFailed(href: Url, error: ReadError) { Timber.e(error.toDebugDescription()) activityChannel.send( - ActivityCommand.ToastError( - ReaderUserError(ReadUserError(error)) - ) + ActivityCommand.ToastError(error.toUserError()) ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt index 0439791ba1..1be697ccad 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/VisualReaderFragment.kt @@ -56,7 +56,6 @@ import org.readium.r2.testapp.data.model.Highlight import org.readium.r2.testapp.databinding.FragmentReaderBinding import org.readium.r2.testapp.reader.preferences.UserPreferencesBottomSheetDialogFragment import org.readium.r2.testapp.reader.tts.TtsControls -import org.readium.r2.testapp.reader.tts.TtsUserError import org.readium.r2.testapp.reader.tts.TtsViewModel import org.readium.r2.testapp.utils.* import org.readium.r2.testapp.utils.extensions.confirmDialog @@ -220,7 +219,7 @@ abstract class VisualReaderFragment : BaseReaderFragment() { when (event) { is TtsViewModel.Event.OnError -> { Timber.e(event.error.toDebugDescription()) - showError(TtsUserError(event.error)) + showError(event.error.toUserError()) } is TtsViewModel.Event.OnMissingVoiceData -> confirmAndInstallTtsVoice(event.language) diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsError.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsError.kt index 9e6e09857f..78afcc3dbe 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsError.kt @@ -12,6 +12,8 @@ import org.readium.navigator.media.tts.android.AndroidTtsEngine import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.testapp.R +import org.readium.r2.testapp.utils.UserError @OptIn(ExperimentalReadiumApi::class) sealed class TtsError( @@ -37,4 +39,12 @@ sealed class TtsError( class Initialization(override val cause: TtsNavigatorFactory.Error) : TtsError(cause.message, cause) + + fun toUserError(): UserError = when (this) { + is ContentError -> UserError(R.string.tts_error_other) + is EngineError.Network -> UserError(R.string.tts_error_network) + is EngineError.Other -> UserError(R.string.tts_error_other) + is Initialization -> UserError(R.string.tts_error_initialization) + is ServiceError -> UserError(R.string.error_unexpected) + } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsUserError.kt deleted file mode 100644 index f64caad0f4..0000000000 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/tts/TtsUserError.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.testapp.reader.tts - -import androidx.annotation.StringRes -import org.readium.navigator.media.tts.TtsNavigator -import org.readium.navigator.media.tts.android.AndroidTtsEngine -import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.util.Error -import org.readium.r2.testapp.R -import org.readium.r2.testapp.utils.UserError - -@OptIn(ExperimentalReadiumApi::class) -sealed class TtsUserError( - override val content: UserError.Content, - override val cause: UserError? = null -) : UserError { - - constructor(@StringRes userMessageId: Int) : - this(UserError.Content(userMessageId), null) - - class ContentError(val error: TtsNavigator.Error.ContentError) : - TtsUserError(R.string.tts_error_other) - - sealed class EngineError(@StringRes userMessageId: Int) : TtsUserError(userMessageId) { - - class Network(val error: AndroidTtsEngine.Error.Network) : - EngineError(R.string.tts_error_network) - - class Other(val error: AndroidTtsEngine.Error) : - EngineError(R.string.tts_error_other) - } - - class ServiceError(val error: Error?) : - TtsUserError(R.string.error_unexpected) - - class Initialization(val error: Error) : - TtsUserError(R.string.tts_error_initialization) - - companion object { - - operator fun invoke(error: TtsError): TtsUserError = - when (error) { - is TtsError.ContentError -> - ContentError(error.cause) - is TtsError.EngineError.Network -> - EngineError.Network(error.cause) - is TtsError.EngineError.Other -> - EngineError.Other(error.cause) - is TtsError.Initialization -> - Initialization(error.cause) - is TtsError.ServiceError -> - ServiceError(error.cause) - } - } -} diff --git a/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt index da80e503e0..d0f02426de 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/search/SearchUserError.kt @@ -6,37 +6,13 @@ package org.readium.r2.testapp.search -import androidx.annotation.StringRes import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.services.search.SearchError -import org.readium.r2.shared.util.Error import org.readium.r2.testapp.R import org.readium.r2.testapp.utils.UserError -sealed class SearchUserError( - override val content: UserError.Content, - override val cause: UserError? = null -) : UserError { - constructor(@StringRes userMessageId: Int) : - this(UserError.Content(userMessageId), null) - object PublicationNotSearchable : - SearchUserError(R.string.search_error_not_searchable) - - class Reading(val error: Error) : - SearchUserError(R.string.search_error_other) - - class Engine(val error: Error) : - SearchUserError(R.string.search_error_other) - - companion object { - - @OptIn(ExperimentalReadiumApi::class) - operator fun invoke(error: SearchError): SearchUserError = - when (error) { - is SearchError.Reading -> - Reading(error) - is SearchError.Engine -> - Engine(error) - } - } +@OptIn(ExperimentalReadiumApi::class) +fun SearchError.toUserError(): UserError = when (this) { + is SearchError.Engine -> UserError(R.string.search_error_other) + is SearchError.Reading -> UserError(R.string.search_error_other) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/utils/UserError.kt b/test-app/src/main/java/org/readium/r2/testapp/utils/UserError.kt index 733598101a..79a0bb3cdd 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/utils/UserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/utils/UserError.kt @@ -14,13 +14,37 @@ import java.util.Date import org.joda.time.DateTime /** - * An exception that can be presented to the user using a localized message. + * An error that can be presented to the user using a localized message. */ -interface UserError { +class UserError private constructor( + val content: Content, + val cause: UserError? +) { - val content: Content + constructor(@StringRes userMessageId: Int, vararg args: Any?, cause: UserError? = null) : + this(Content(userMessageId, *args), cause) - val cause: UserError? + constructor( + @PluralsRes userMessageId: Int, + quantity: Int?, + vararg args: Any?, + cause: UserError? = null + ) : + this(Content(userMessageId, quantity, *args), cause) + + constructor(message: String, cause: UserError? = null) : + this(Content(message), cause) + + constructor(cause: UserError) : + this(Content.CauseUserError(cause), cause) + + /** + * Gets the localized user-facing message for this exception. + * + * @param includesCauses Includes nested [UserError] causes in the user message when true. + */ + fun getUserMessage(context: Context, includesCauses: Boolean = true): String = + content.getUserMessage(context, cause, includesCauses) /** * Provides a way to generate a localized user message. @@ -36,13 +60,13 @@ interface UserError { /** * Holds a nested [UserError]. */ - class Error(val error: UserError) : Content() { + class CauseUserError(val exception: UserError) : Content() { override fun getUserMessage( context: Context, cause: UserError?, includesCauses: Boolean ): String = - error.getUserMessage(context, includesCauses) + exception.getUserMessage(context, includesCauses) } /** @@ -83,7 +107,7 @@ interface UserError { } if (cause != null && includesCauses) { - message += ": ${cause.getUserMessage(context, true)}" + message += ": ${cause.getUserMessage(context, includesCauses)}" } return message @@ -111,18 +135,9 @@ interface UserError { vararg args: Any? ): Content = LocalizedString(userMessageId, args, quantity) - operator fun invoke(cause: UserError): Content = - Error(cause) + operator fun invoke(message: String): Content = Message(message) } } } - -/** - * Gets the localized user-facing message for this exception. - * - * @param includesCauses Includes nested [UserError] causes in the user message when true. - */ -fun UserError.getUserMessage(context: Context, includesCauses: Boolean = true): String = - content.getUserMessage(context, cause, includesCauses) From cc1971755d80e7bb451f8bacc53fc41299a384bc Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Fri, 8 Dec 2023 05:43:53 +0100 Subject: [PATCH 65/86] Fix DownloadManager errors --- .../shared/util/downloads/DownloadManager.kt | 15 ++--- .../android/AndroidDownloadManager.kt | 25 +++++--- .../foreground/ForegroundDownloadManager.kt | 60 ++++++++++++------- .../r2/shared/util/file/FileResource.kt | 2 +- .../r2/shared/util/file/FileSystemError.kt | 8 ++- .../shared/util/zip/FileZipArchiveProvider.kt | 2 +- .../r2/testapp/domain/ReadUserError.kt | 5 +- test-app/src/main/res/values/strings.xml | 1 + 8 files changed, 71 insertions(+), 47 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt index 822680b90e..1beab01aae 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/DownloadManager.kt @@ -11,6 +11,7 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager import org.readium.r2.shared.util.downloads.foreground.ForegroundDownloadManager +import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.mediatype.MediaType /** @@ -42,24 +43,16 @@ public interface DownloadManager { override val cause: Error? = null ) : Error { - public class HttpError( + public class Http( cause: org.readium.r2.shared.util.http.HttpError ) : DownloadError(cause.message, cause) - public class DeviceNotFound( - cause: Error? = null - ) : DownloadError("The storage device is missing.", cause) - public class CannotResume( cause: Error? = null ) : DownloadError("Download couldn't be resumed.", cause) - public class InsufficientSpace( - cause: Error? = null - ) : DownloadError("There is not enough space to complete the download.", cause) - - public class FileSystemError( - cause: Error? = null + public class FileSystem( + override val cause: FileSystemError ) : DownloadError("IO error on the local device.", cause) public class Unknown( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index fecbc5e5fa..758351fd19 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -27,6 +27,7 @@ import org.readium.r2.shared.extensions.tryOr import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpStatus import org.readium.r2.shared.util.mediatype.FormatRegistry @@ -292,8 +293,8 @@ public class AndroidDownloadManager internal constructor( Try.success(download) } else { Try.failure( - DownloadManager.DownloadError.FileSystemError( - DebugError("Failed to rename the downloaded file.") + DownloadManager.DownloadError.FileSystem( + FileSystemError.IO(DebugError("Failed to rename the downloaded file.")) ) ) } @@ -302,23 +303,29 @@ public class AndroidDownloadManager internal constructor( private fun mapErrorCode(code: Int): DownloadManager.DownloadError = when (code) { in 400 until 1000 -> - DownloadManager.DownloadError.HttpError(httpErrorForCode(code)) + DownloadManager.DownloadError.Http(httpErrorForCode(code)) SystemDownloadManager.ERROR_UNHANDLED_HTTP_CODE -> - DownloadManager.DownloadError.HttpError(httpErrorForCode(code)) + DownloadManager.DownloadError.Http(httpErrorForCode(code)) SystemDownloadManager.ERROR_HTTP_DATA_ERROR -> - DownloadManager.DownloadError.HttpError(HttpError.MalformedResponse(null)) + DownloadManager.DownloadError.Http(HttpError.MalformedResponse(null)) SystemDownloadManager.ERROR_TOO_MANY_REDIRECTS -> - DownloadManager.DownloadError.HttpError( + DownloadManager.DownloadError.Http( HttpError.Redirection(DebugError("Too many redirects.")) ) SystemDownloadManager.ERROR_CANNOT_RESUME -> DownloadManager.DownloadError.CannotResume() SystemDownloadManager.ERROR_DEVICE_NOT_FOUND -> - DownloadManager.DownloadError.DeviceNotFound() + DownloadManager.DownloadError.FileSystem( + FileSystemError.FileNotFound( + DebugError("Missing device.") + ) + ) SystemDownloadManager.ERROR_FILE_ERROR -> - DownloadManager.DownloadError.FileSystemError() + DownloadManager.DownloadError.FileSystem( + FileSystemError.IO(DebugError("An error occurred on the filesystem.")) + ) SystemDownloadManager.ERROR_INSUFFICIENT_SPACE -> - DownloadManager.DownloadError.InsufficientSpace() + DownloadManager.DownloadError.FileSystem(FileSystemError.InsufficientSpace()) SystemDownloadManager.ERROR_UNKNOWN -> DownloadManager.DownloadError.Unknown() else -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt index c3670ddd2f..47f409b1f2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt @@ -20,12 +20,12 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpRequest import org.readium.r2.shared.util.http.HttpResponse -import org.readium.r2.shared.util.http.HttpTry /** * A [DownloadManager] implementation using a [HttpClient]. @@ -34,7 +34,7 @@ import org.readium.r2.shared.util.http.HttpTry */ public class ForegroundDownloadManager( private val httpClient: HttpClient, - private val bufferLength: Int = 1024 * 8 + private val downloadsDirectory: File ) : DownloadManager { private val coroutineScope: CoroutineScope = @@ -58,7 +58,7 @@ public class ForegroundDownloadManager( private suspend fun doRequest(request: DownloadManager.Request, id: DownloadManager.RequestId) { val destination = withContext(Dispatchers.IO) { - File.createTempFile(UUID.randomUUID().toString(), null) + File.createTempFile(UUID.randomUUID().toString(), null, downloadsDirectory) } httpClient @@ -87,7 +87,7 @@ public class ForegroundDownloadManager( } .onFailure { error -> forEachListener(id) { - onDownloadFailed(id, DownloadManager.DownloadError.HttpError(error)) + onDownloadFailed(id, error) } } @@ -124,30 +124,44 @@ public class ForegroundDownloadManager( request: HttpRequest, destination: File, onProgress: (downloaded: Long, expected: Long?) -> Unit - ): HttpTry = + ): Try = try { - stream(request).flatMap { res -> - withContext(Dispatchers.IO) { - val expected = res.response.contentLength?.takeIf { it > 0 } - - res.body.use { input -> - FileOutputStream(destination).use { output -> - val buf = ByteArray(bufferLength) - var n: Int - var downloadedBytes = 0L - while (-1 != input.read(buf).also { n = it }) { - ensureActive() - downloadedBytes += n - output.write(buf, 0, n) - onProgress(downloadedBytes, expected) + stream(request) + .mapFailure { DownloadManager.DownloadError.Http(it) } + .flatMap { res -> + withContext(Dispatchers.IO) { + val expected = res.response.contentLength?.takeIf { it > 0 } + val freespace = destination.freeSpace.takeUnless { it == 0L } + + if (expected != null && freespace != null && destination.freeSpace < expected) { + return@withContext Try.failure( + DownloadManager.DownloadError.FileSystem( + FileSystemError.InsufficientSpace( + requiredSpace = expected, + freespace = freespace + ) + ) + ) + } + + res.body.use { input -> + FileOutputStream(destination).use { output -> + val buf = ByteArray(DEFAULT_BUFFER_SIZE) + var n: Int + var downloadedBytes = 0L + while (-1 != input.read(buf).also { n = it }) { + ensureActive() + downloadedBytes += n + output.write(buf, 0, n) + onProgress(downloadedBytes, expected) + } } } - } - Try.success(res.response) + Try.success(res.response) + } } - } } catch (e: IOException) { - Try.failure(HttpError.IO(e)) + Try.failure(DownloadManager.DownloadError.Http(HttpError.IO(e))) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt index ec33059a0f..19b91ae2d0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResource.kt @@ -124,7 +124,7 @@ public class FileResource( try { success(closure()) } catch (e: FileNotFoundException) { - failure(ReadError.Access(FileSystemError.NotFound(e))) + failure(ReadError.Access(FileSystemError.FileNotFound(e))) } catch (e: SecurityException) { failure(ReadError.Access(FileSystemError.Forbidden(e))) } catch (e: IOException) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileSystemError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileSystemError.kt index 1f94583104..be96d1ccb8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileSystemError.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileSystemError.kt @@ -18,7 +18,7 @@ public sealed class FileSystemError( override val cause: Error? = null ) : AccessError { - public class NotFound( + public class FileNotFound( cause: Error? ) : FileSystemError("File not found.", cause) { @@ -38,4 +38,10 @@ public sealed class FileSystemError( public constructor(exception: Exception) : this(ThrowableError(exception)) } + + public class InsufficientSpace( + public val requiredSpace: Long? = null, + public val freespace: Long? = null, + cause: Error? = null + ) : FileSystemError("There is not enough space to do the operation.", cause) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index 493c30c764..8759a45027 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -76,7 +76,7 @@ internal class FileZipArchiveProvider { } catch (e: FileNotFoundException) { Try.failure( ArchiveFactory.Error.Reading( - ReadError.Access(FileSystemError.NotFound(e)) + ReadError.Access(FileSystemError.FileNotFound(e)) ) ) } catch (e: ZipException) { diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt index 048a80ba46..45abd317d9 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ReadUserError.kt @@ -44,7 +44,10 @@ fun HttpError.toUserError(): UserError = when (this) { fun FileSystemError.toUserError(): UserError = when (this) { is FileSystemError.Forbidden -> UserError(R.string.publication_error_filesystem_unexpected) is FileSystemError.IO -> UserError(R.string.publication_error_filesystem_unexpected) - is FileSystemError.NotFound -> UserError(R.string.publication_error_filesystem_not_found) + is FileSystemError.InsufficientSpace -> UserError( + R.string.publication_error_filesystem_insufficient_space + ) + is FileSystemError.FileNotFound -> UserError(R.string.publication_error_filesystem_not_found) } fun ContentResolverError.toUserError(): UserError = when (this) { diff --git a/test-app/src/main/res/values/strings.xml b/test-app/src/main/res/values/strings.xml index c1d54eb444..b71298e553 100644 --- a/test-app/src/main/res/values/strings.xml +++ b/test-app/src/main/res/values/strings.xml @@ -107,6 +107,7 @@ An unexpected network error occurred. A file has not been found. An unexpected filesystem error occurred. + There is not enough space left on the device. Provided credentials were incorrect You are not allowed to open this publication There is not enough memory on this device to open the publication. From 71008908c127952d77f52e5c1405d9403760c192 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Fri, 8 Dec 2023 05:58:46 +0100 Subject: [PATCH 66/86] Small fix --- .../foreground/ForegroundDownloadManager.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt index 47f409b1f2..dd34f94f7d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt @@ -130,21 +130,21 @@ public class ForegroundDownloadManager( .mapFailure { DownloadManager.DownloadError.Http(it) } .flatMap { res -> withContext(Dispatchers.IO) { - val expected = res.response.contentLength?.takeIf { it > 0 } - val freespace = destination.freeSpace.takeUnless { it == 0L } - - if (expected != null && freespace != null && destination.freeSpace < expected) { - return@withContext Try.failure( - DownloadManager.DownloadError.FileSystem( - FileSystemError.InsufficientSpace( - requiredSpace = expected, - freespace = freespace + res.body.use { input -> + val expected = res.response.contentLength?.takeIf { it > 0 } + val freespace = destination.freeSpace.takeUnless { it == 0L } + + if (expected != null && freespace != null && destination.freeSpace < expected) { + return@withContext Try.failure( + DownloadManager.DownloadError.FileSystem( + FileSystemError.InsufficientSpace( + requiredSpace = expected, + freespace = freespace + ) ) ) - ) - } + } - res.body.use { input -> FileOutputStream(destination).use { output -> val buf = ByteArray(DEFAULT_BUFFER_SIZE) var n: Int From ce4dbd55748377a03c477500cd6c16ce83becc49 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Fri, 8 Dec 2023 06:43:09 +0100 Subject: [PATCH 67/86] Small fix --- .../org/readium/r2/shared/util/content/ContentResource.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt index 70d9eed2dc..c8a2ffdd05 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt @@ -141,9 +141,7 @@ public class ContentResource( ContentResolverError.NotAvailable() ) ) - val result = block(stream) - stream.close() - result + stream.use { block(stream) } } } From f65b677a336cfe70dbbc5b425f15b570c1912d4f Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Fri, 8 Dec 2023 14:24:07 +0100 Subject: [PATCH 68/86] Various changes --- .../src/main/assets/readium/error.xhtml | 10 +++----- .../assets/readium/webview_error_icon.svg | 1 + .../r2/navigator/epub/WebViewServer.kt | 11 ++++----- .../r2/navigator/media/ExoMediaPlayer.kt | 4 ++-- .../readium/r2/shared/extensions/Exception.kt | 17 ++++--------- .../r2/shared/util/archive/ArchiveFactory.kt | 14 +++++------ .../util/archive/RecursiveArchiveFactory.kt | 6 ++--- .../r2/shared/util/asset/AssetRetriever.kt | 8 +++---- .../readium/r2/shared/util/data/Decoding.kt | 1 + .../util/mediatype/MediaTypeRetriever.kt | 4 ++-- .../shared/util/mediatype/MediaTypeSniffer.kt | 4 ++-- .../shared/util/zip/FileZipArchiveProvider.kt | 14 +++++------ .../util/zip/StreamingZipArchiveProvider.kt | 24 +++++++------------ .../shared/util/zip/StreamingZipContainer.kt | 11 ++++----- .../r2/shared/util/zip/ZipArchiveFactory.kt | 2 +- .../util/mediatype/MediaTypeRetrieverTest.kt | 8 +++++++ test-app/src/main/assets/readium/error.xhtml | 8 ++----- .../assets/readium/webview_error_icon.svg | 1 + .../readium/r2/testapp/domain/Bookshelf.kt | 10 ++++---- .../readium/r2/testapp/domain/ImportError.kt | 20 ++++++++++------ .../r2/testapp/domain/PublicationRetriever.kt | 14 +++++------ 21 files changed, 90 insertions(+), 102 deletions(-) create mode 100644 readium/navigator/src/main/assets/readium/webview_error_icon.svg create mode 100644 test-app/src/main/assets/readium/webview_error_icon.svg diff --git a/readium/navigator/src/main/assets/readium/error.xhtml b/readium/navigator/src/main/assets/readium/error.xhtml index 66d7f90ca8..32025f280d 100644 --- a/readium/navigator/src/main/assets/readium/error.xhtml +++ b/readium/navigator/src/main/assets/readium/error.xhtml @@ -1,13 +1,9 @@ + "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> - - ${error} - - -

${error}

-

${href}

+ +

Error

diff --git a/readium/navigator/src/main/assets/readium/webview_error_icon.svg b/readium/navigator/src/main/assets/readium/webview_error_icon.svg new file mode 100644 index 0000000000..b518ba004d --- /dev/null +++ b/readium/navigator/src/main/assets/readium/webview_error_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt index f4461f5468..7a4047c264 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/epub/WebViewServer.kt @@ -94,13 +94,13 @@ internal class WebViewServer( .get(urlWithoutAnchor) ?.fallback { onResourceLoadFailed(urlWithoutAnchor, it) - errorResource(urlWithoutAnchor, it) + errorResource() } ?: run { val error = ReadError.Decoding( "Resource not found at $urlWithoutAnchor in publication." ) onResourceLoadFailed(urlWithoutAnchor, error) - errorResource(urlWithoutAnchor, error) + errorResource() } link.mediaType @@ -146,15 +146,14 @@ internal class WebViewServer( ) } } - private fun errorResource(url: Url, error: ReadError): Resource = + private fun errorResource(): Resource = StringResource { withContext(Dispatchers.IO) { Try.success( application.assets - .open("readium/error.xhtml").bufferedReader() + .open("readium/error.xhtml") + .bufferedReader() .use { it.readText() } - .replace("\${error}", error.message) - .replace("\${href}", url.toString()) ) } } diff --git a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt index 16cf2a0f50..9ca55d6450 100644 --- a/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt +++ b/readium/navigator/src/main/java/org/readium/r2/navigator/media/ExoMediaPlayer.kt @@ -45,7 +45,7 @@ import kotlinx.coroutines.launch import org.readium.r2.navigator.ExperimentalAudiobook import org.readium.r2.navigator.R import org.readium.r2.navigator.extensions.timeWithDuration -import org.readium.r2.shared.extensions.asInstance +import org.readium.r2.shared.extensions.findInstance import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication @@ -197,7 +197,7 @@ public class ExoMediaPlayer( } override fun onPlayerError(error: PlaybackException) { - val readError = error.asInstance()?.error + val readError = error.findInstance()?.error if (readError != null) { player.currentMediaItem?.mediaId diff --git a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Exception.kt b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Exception.kt index cad1fc49b5..b37ea12bfd 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/extensions/Exception.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/extensions/Exception.kt @@ -41,25 +41,16 @@ public inline fun tryOrLog(closure: () -> T): T? = * Finds the first cause instance of the given type. */ @InternalReadiumApi -public inline fun Throwable.asInstance(): T? = - asInstance(T::class.java) +public inline fun Throwable.findInstance(): T? = + findInstance(T::class.java) /** * Finds the first cause instance of the given type. */ @InternalReadiumApi -public fun Throwable.asInstance(klass: Class): R? = +public fun Throwable.findInstance(klass: Class): R? = @Suppress("UNCHECKED_CAST") when { klass.isInstance(this) -> this as R - else -> cause?.asInstance(klass) + else -> cause?.findInstance(klass) } - -/** - * Unwraps the nearest instance of [klass] if any. - */ -@InternalReadiumApi -public fun Exception.unwrapInstance(klass: Class): Exception { - asInstance(klass)?.let { return it } - return this -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt index 11cc15feaa..ef7572a35d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt @@ -18,7 +18,7 @@ import org.readium.r2.shared.util.resource.Resource */ public interface ArchiveFactory { - public sealed class Error( + public sealed class CreateError( override val message: String, override val cause: org.readium.r2.shared.util.Error? ) : org.readium.r2.shared.util.Error { @@ -26,11 +26,11 @@ public interface ArchiveFactory { public class FormatNotSupported( public val mediaType: MediaType, cause: org.readium.r2.shared.util.Error? = null - ) : Error("Media type not supported.", cause) + ) : CreateError("Media type not supported.", cause) public class Reading( override val cause: org.readium.r2.shared.util.data.ReadError - ) : Error("An error occurred while attempting to read the resource.", cause) + ) : CreateError("An error occurred while attempting to read the resource.", cause) } /** @@ -39,7 +39,7 @@ public interface ArchiveFactory { public suspend fun create( mediaType: MediaType, source: Readable - ): Try, Error> + ): Try, CreateError> } /** @@ -56,18 +56,18 @@ public class CompositeArchiveFactory( override suspend fun create( mediaType: MediaType, source: Readable - ): Try, ArchiveFactory.Error> { + ): Try, ArchiveFactory.CreateError> { for (factory in factories) { factory.create(mediaType, source) .getOrElse { error -> when (error) { - is ArchiveFactory.Error.FormatNotSupported -> null + is ArchiveFactory.CreateError.FormatNotSupported -> null else -> return Try.failure(error) } } ?.let { return Try.success(it) } } - return Try.failure(ArchiveFactory.Error.FormatNotSupported(mediaType)) + return Try.failure(ArchiveFactory.CreateError.FormatNotSupported(mediaType)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt index 05c49d4681..419add010e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt @@ -26,16 +26,16 @@ internal class RecursiveArchiveFactory( override suspend fun create( mediaType: MediaType, source: Readable - ): Try, ArchiveFactory.Error> = + ): Try, ArchiveFactory.CreateError> = archiveFactory.create(mediaType, source) .tryRecover { error -> when (error) { - is ArchiveFactory.Error.FormatNotSupported -> { + is ArchiveFactory.CreateError.FormatNotSupported -> { formatRegistry.superType(mediaType) ?.let { create(it, source) } ?: Try.failure(error) } - is ArchiveFactory.Error.Reading -> + is ArchiveFactory.CreateError.Reading -> Try.failure(error) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index 683f836c7f..c5a4eb3d6f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -66,9 +66,9 @@ public class AssetRetriever( val archive = archiveFactory.create(mediaType, resource) .getOrElse { return when (it) { - is ArchiveFactory.Error.Reading -> + is ArchiveFactory.CreateError.Reading -> Try.failure(RetrieveError.Reading(it.cause)) - is ArchiveFactory.Error.FormatNotSupported -> + is ArchiveFactory.CreateError.FormatNotSupported -> Try.success(ResourceAsset(mediaType, resource)) } } @@ -123,9 +123,9 @@ public class AssetRetriever( val container = archiveFactory.create(mediaType, resource) .getOrElse { when (it) { - is ArchiveFactory.Error.Reading -> + is ArchiveFactory.CreateError.Reading -> return Try.failure(RetrieveError.Reading(it.cause)) - is ArchiveFactory.Error.FormatNotSupported -> + is ArchiveFactory.CreateError.FormatNotSupported -> return Try.success(ResourceAsset(mediaType, resource)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index 815c236124..f32bced4cc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -52,6 +52,7 @@ public sealed class DecodeError( /** * Decodes receiver properly wrapping exceptions into [DecodeError]s. */ +@InternalReadiumApi public suspend fun S.decode( block: (value: S) -> R, wrapError: (Exception) -> Error diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt index d76303cc2e..a9b9fc163f 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt @@ -74,9 +74,9 @@ public class MediaTypeRetriever( val container = archiveFactory.create(resourceMediaType, resource) .getOrElse { when (it) { - is ArchiveFactory.Error.Reading -> + is ArchiveFactory.CreateError.Reading -> return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - is ArchiveFactory.Error.FormatNotSupported -> + is ArchiveFactory.CreateError.FormatNotSupported -> return Try.success(resourceMediaType) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt index fffd31a233..783a775e2a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt @@ -13,7 +13,7 @@ import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject -import org.readium.r2.shared.extensions.asInstance +import org.readium.r2.shared.extensions.findInstance import org.readium.r2.shared.extensions.tryOrNull import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Manifest @@ -845,7 +845,7 @@ public object SystemMediaTypeSniffer : MediaTypeSniffer { ?.let { sniffType(it) } } } catch (e: Exception) { - e.asInstance(SystemSnifferException::class.java) + e.findInstance(SystemSnifferException::class.java) ?.let { return Try.failure( MediaTypeSnifferError.Reading(it.error) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index 8759a45027..7ae50ac0da 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -54,10 +54,10 @@ internal class FileZipArchiveProvider { suspend fun create( mediaType: MediaType, file: File - ): Try, ArchiveFactory.Error> { + ): Try, ArchiveFactory.CreateError> { if (mediaType != MediaType.ZIP) { return Try.failure( - ArchiveFactory.Error.FormatNotSupported(mediaType) + ArchiveFactory.CreateError.FormatNotSupported(mediaType) ) } @@ -68,32 +68,32 @@ internal class FileZipArchiveProvider { } // Internal for testing purpose - internal suspend fun open(file: File): Try, ArchiveFactory.Error> = + internal suspend fun open(file: File): Try, ArchiveFactory.CreateError> = withContext(Dispatchers.IO) { try { val archive = FileZipContainer(ZipFile(file), file) Try.success(archive) } catch (e: FileNotFoundException) { Try.failure( - ArchiveFactory.Error.Reading( + ArchiveFactory.CreateError.Reading( ReadError.Access(FileSystemError.FileNotFound(e)) ) ) } catch (e: ZipException) { Try.failure( - ArchiveFactory.Error.Reading( + ArchiveFactory.CreateError.Reading( ReadError.Decoding(e) ) ) } catch (e: SecurityException) { Try.failure( - ArchiveFactory.Error.Reading( + ArchiveFactory.CreateError.Reading( ReadError.Access(FileSystemError.Forbidden(e)) ) ) } catch (e: IOException) { Try.failure( - ArchiveFactory.Error.Reading( + ArchiveFactory.CreateError.Reading( ReadError.Access(FileSystemError.IO(e)) ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 0df72d0585..57468b3a16 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -10,7 +10,7 @@ import java.io.File import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.readium.r2.shared.extensions.unwrapInstance +import org.readium.r2.shared.extensions.findInstance import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.archive.ArchiveFactory @@ -36,22 +36,19 @@ internal class StreamingZipArchiveProvider { openBlob(readable, ::ReadException, null) Try.success(MediaType.ZIP) } catch (exception: Exception) { - when (val e = exception.unwrapInstance(ReadException::class.java)) { - is ReadException -> - Try.failure(MediaTypeSnifferError.Reading(e.error)) - else -> - Try.failure(MediaTypeSnifferError.NotRecognized) - } + exception.findInstance(ReadException::class.java) + ?.let { Try.failure(MediaTypeSnifferError.Reading(it.error)) } + ?: Try.failure(MediaTypeSnifferError.NotRecognized) } } suspend fun create( mediaType: MediaType, readable: Readable - ): Try, ArchiveFactory.Error> { + ): Try, ArchiveFactory.CreateError> { if (mediaType != MediaType.ZIP) { return Try.failure( - ArchiveFactory.Error.FormatNotSupported(mediaType) + ArchiveFactory.CreateError.FormatNotSupported(mediaType) ) } @@ -63,12 +60,9 @@ internal class StreamingZipArchiveProvider { ) Try.success(container) } catch (exception: Exception) { - when (val e = exception.unwrapInstance(ReadException::class.java)) { - is ReadException -> - Try.failure(ArchiveFactory.Error.Reading(e.error)) - else -> - Try.failure(ArchiveFactory.Error.Reading(ReadError.Decoding(e))) - } + exception.findInstance(ReadException::class.java) + ?.let { Try.failure(ArchiveFactory.CreateError.Reading(it.error)) } + ?: Try.failure(ArchiveFactory.CreateError.Reading(ReadError.Decoding(exception))) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt index 13c754958a..fa2332b585 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt @@ -8,9 +8,9 @@ package org.readium.r2.shared.util.zip import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.readium.r2.shared.extensions.findInstance import org.readium.r2.shared.extensions.readFully import org.readium.r2.shared.extensions.tryOrLog -import org.readium.r2.shared.extensions.unwrapInstance import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.RelativeUrl @@ -82,12 +82,9 @@ internal class StreamingZipContainer( } Try.success(bytes) } catch (exception: Exception) { - when (val e = exception.unwrapInstance(ReadException::class.java)) { - is ReadException -> - Try.failure(e.error) - else -> - Try.failure(ReadError.Decoding(e)) - } + exception.findInstance(ReadException::class.java) + ?.let { Try.failure(it.error) } + ?: Try.failure(ReadError.Decoding(exception)) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt index e5f6caa2e1..7e02aa7057 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt @@ -22,7 +22,7 @@ public class ZipArchiveFactory : ArchiveFactory { override suspend fun create( mediaType: MediaType, source: Readable - ): Try, ArchiveFactory.Error> = + ): Try, ArchiveFactory.CreateError> = (source as? Resource)?.sourceUrl?.toFile() ?.let { fileZipArchiveProvider.create(mediaType, it) } ?: streamingZipArchiveProvider.create(mediaType, source) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt index 09d5c47b24..b9023a8d0b 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt @@ -248,6 +248,14 @@ class MediaTypeRetrieverTest { assertEquals(MediaType.JXL, retriever.retrieve(mediaType = "image/jxl")) } + @Test + fun `sniff RAR`() = runBlocking { + assertEquals(MediaType.RAR, retriever.retrieve(fileExtension = "rar")) + assertEquals(MediaType.RAR, retriever.retrieve(mediaType = "application/vnd.rar")) + assertEquals(MediaType.RAR, retriever.retrieve(mediaType = "application/x-rar")) + assertEquals(MediaType.RAR, retriever.retrieve(mediaType = "application/x-rar-compressed")) + } + @Test fun `sniff OPDS 1 feed`() = runBlocking { assertEquals( diff --git a/test-app/src/main/assets/readium/error.xhtml b/test-app/src/main/assets/readium/error.xhtml index 66d7f90ca8..18d09b5d03 100644 --- a/test-app/src/main/assets/readium/error.xhtml +++ b/test-app/src/main/assets/readium/error.xhtml @@ -3,11 +3,7 @@ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> - - ${error} - - -

${error}

-

${href}

+ +

Error

diff --git a/test-app/src/main/assets/readium/webview_error_icon.svg b/test-app/src/main/assets/readium/webview_error_icon.svg new file mode 100644 index 0000000000..b518ba004d --- /dev/null +++ b/test-app/src/main/assets/readium/webview_error_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index ae34d513d8..c70d2c4340 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -129,7 +129,7 @@ class Bookshelf( assetRetriever.retrieve(url) .getOrElse { return Try.failure( - ImportError.PublicationError(PublicationError(it)) + ImportError.Publication(PublicationError(it)) ) } @@ -144,7 +144,7 @@ class Bookshelf( } }.getOrElse { return Try.failure( - ImportError.PublicationError(PublicationError(it)) + ImportError.Publication(PublicationError(it)) ) } @@ -157,7 +157,7 @@ class Bookshelf( coverStorage.storeCover(publication, coverUrl) .getOrElse { return Try.failure( - ImportError.PublicationError( + ImportError.Publication( PublicationError.ReadError(ReadError.Access(FileSystemError.IO(it))) ) ) @@ -173,7 +173,7 @@ class Bookshelf( if (id == -1L) { coverFile.delete() return Try.failure( - ImportError.DatabaseError( + ImportError.Database( DebugError("Could not insert book into database.") ) ) @@ -182,7 +182,7 @@ class Bookshelf( .onFailure { Timber.e("Cannot open publication: $it.") return Try.failure( - ImportError.PublicationError(PublicationError(it)) + ImportError.Publication(PublicationError(it)) ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt index 0cfc1abbc9..8e9278649c 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt @@ -9,6 +9,7 @@ package org.readium.r2.testapp.domain import org.readium.r2.lcp.LcpError import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.testapp.R import org.readium.r2.testapp.utils.UserError @@ -23,25 +24,30 @@ sealed class ImportError( override val cause: LcpError ) : ImportError(cause) - class PublicationError( - override val cause: org.readium.r2.testapp.domain.PublicationError + class Publication( + override val cause: PublicationError + ) : ImportError(cause) + + class FileSystem( + override val cause: FileSystemError ) : ImportError(cause) class DownloadFailed( override val cause: DownloadManager.DownloadError ) : ImportError(cause) - class OpdsError(override val cause: Error) : + class Opds(override val cause: Error) : ImportError(cause) - class DatabaseError(override val cause: Error) : + class Database(override val cause: Error) : ImportError(cause) fun toUserError(): UserError = when (this) { - is DatabaseError -> UserError(R.string.import_publication_unable_add_pub_database) + is Database -> UserError(R.string.import_publication_unable_add_pub_database) is DownloadFailed -> UserError(R.string.import_publication_download_failed) is LcpAcquisitionFailed -> cause.toUserError() - is OpdsError -> UserError(R.string.import_publication_no_acquisition) - is PublicationError -> cause.toUserError() + is Opds -> UserError(R.string.import_publication_no_acquisition) + is Publication -> cause.toUserError() + is FileSystem -> cause.toUserError() } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index 3579ac2f30..5afe91eb0e 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -126,9 +126,7 @@ class LocalPublicationRetriever( val tempFile = uri.copyToTempFile(context, storageDir) .getOrElse { listener.onError( - ImportError.PublicationError( - PublicationError.ReadError(ReadError.Access(FileSystemError.IO(it))) - ) + ImportError.FileSystem(FileSystemError.IO(it)) ) return@launch } @@ -155,7 +153,7 @@ class LocalPublicationRetriever( val sourceAsset = assetRetriever.retrieve(tempFile) .getOrElse { listener.onError( - ImportError.PublicationError(PublicationError(it)) + ImportError.Publication(PublicationError(it)) ) return } @@ -166,7 +164,7 @@ class LocalPublicationRetriever( ) { if (lcpPublicationRetriever == null) { listener.onError( - ImportError.PublicationError( + ImportError.Publication( PublicationError.UnsupportedContentProtection( DebugError("LCP support is missing.") ) @@ -188,7 +186,7 @@ class LocalPublicationRetriever( Timber.d(e) tryOrNull { libraryFile.delete() } listener.onError( - ImportError.PublicationError( + ImportError.Publication( PublicationError.ReadError( ReadError.Access(FileSystemError.IO(e)) ) @@ -247,7 +245,7 @@ class OpdsPublicationRetriever( coroutineScope.launch { val publicationUrl = publication.acquisitionUrl() .getOrElse { - listener.onError(ImportError.OpdsError(it)) + listener.onError(ImportError.Opds(it)) return@launch } @@ -356,7 +354,7 @@ class LcpPublicationRetriever( coroutineScope.launch { val license = licenceAsset.resource.read() .getOrElse { - listener.onError(ImportError.PublicationError(PublicationError.ReadError(it))) + listener.onError(ImportError.Publication(PublicationError.ReadError(it))) return@launch } .let { From c8e0066671131b24e77051810e3c1068b7314e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Sun, 10 Dec 2023 20:17:11 +0100 Subject: [PATCH 69/86] Minor changes --- .gitignore | 1 + readium/navigator/src/main/assets/readium/error.xhtml | 6 ++++-- .../src/main/assets/readium/webview_error_icon.svg | 1 - .../publication/protection/ContentProtection.kt | 2 +- .../publication/services/search/SearchService.kt | 2 +- .../readium/r2/shared/util/asset/AssetRetriever.kt | 11 +++++++++++ .../java/org/readium/r2/shared/util/data/Decoding.kt | 2 +- .../java/org/readium/r2/shared/util/data/Reading.kt | 2 +- .../r2/shared/util/file/FileResourceFactory.kt | 2 +- .../org/readium/r2/shared/util/http/HttpRequest.kt | 7 ++++++- .../r2/shared/util/http/HttpResourceFactory.kt | 2 +- .../org/readium/r2/shared/util/mediatype/MediaType.kt | 6 ++---- .../org/readium/r2/shared/util/resource/Resource.kt | 3 +++ .../org/readium/r2/streamer/PublicationFactory.kt | 2 +- test-app/src/main/assets/readium/error.xhtml | 9 --------- .../src/main/assets/readium/webview_error_icon.svg | 1 - 16 files changed, 34 insertions(+), 25 deletions(-) delete mode 100644 readium/navigator/src/main/assets/readium/webview_error_icon.svg delete mode 100644 test-app/src/main/assets/readium/error.xhtml delete mode 100644 test-app/src/main/assets/readium/webview_error_icon.svg diff --git a/.gitignore b/.gitignore index 972d91fb72..d46e9173c1 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ captures/ .idea/libraries .idea/jarRepositories.xml .idea/misc.xml +.idea/migrations.xml # Android Studio 3 in .gitignore file. .idea/caches .idea/modules.xml diff --git a/readium/navigator/src/main/assets/readium/error.xhtml b/readium/navigator/src/main/assets/readium/error.xhtml index 32025f280d..ab268c488c 100644 --- a/readium/navigator/src/main/assets/readium/error.xhtml +++ b/readium/navigator/src/main/assets/readium/error.xhtml @@ -3,7 +3,9 @@ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> - -

Error

+ + + + diff --git a/readium/navigator/src/main/assets/readium/webview_error_icon.svg b/readium/navigator/src/main/assets/readium/webview_error_icon.svg deleted file mode 100644 index b518ba004d..0000000000 --- a/readium/navigator/src/main/assets/readium/webview_error_icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index 2f3570d34e..a9d41be888 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -59,7 +59,7 @@ public interface ContentProtection { /** * Attempts to unlock a potentially protected publication asset. * - * @return A [Asset] in case of success or a [OpenError] if the + * @return A [Asset] in case of success or an [OpenError] if the * asset can't be successfully opened even in restricted mode. */ public suspend fun open( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt index a2842de48e..6806e89938 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/services/search/SearchService.kt @@ -40,7 +40,7 @@ public sealed class SearchError( /** * An error occurring in the search engine. - * */ + */ public class Engine(cause: Error) : SearchError("An error occurred while searching.", cause) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt index c5a4eb3d6f..e9f37bf9e6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -38,14 +38,25 @@ public class AssetRetriever( override val cause: Error? ) : Error { + /** + * The scheme (e.g. http, file, content) for the requested [Url] is not supported by the + * [resourceFactory]. + */ public class SchemeNotSupported( public val scheme: Url.Scheme, cause: Error? = null ) : RetrieveError("Url scheme $scheme is not supported.", cause) + /** + * The format of the resource at the requested [Url] is not recognized by the + * [mediaTypeRetriever] and [archiveFactory]. + */ public class FormatNotSupported(cause: Error) : RetrieveError("Asset format is not supported.", cause) + /** + * An error occurred when trying to read the asset. + */ public class Reading(override val cause: org.readium.r2.shared.util.data.ReadError) : RetrieveError("An error occurred when trying to read asset.", cause) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index f32bced4cc..72953917e7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -98,7 +98,7 @@ public suspend fun ByteArray.decodeJson(): Try = /** * Readium Web Publication Manifest parsed from the content. - * */ + */ public suspend fun ByteArray.decodeRwpm(): Try = decodeJson().flatMap { json -> Manifest.fromJSON(json) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt index e5f0535bac..100aca8470 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Reading.kt @@ -60,7 +60,7 @@ public sealed class ReadError( /** * Content doesn't match what was expected and cannot be interpreted. * - * For instance, this error can be reported if an ZIP archive looks invalid, + * For instance, this error can be reported if a ZIP archive looks invalid, * a publication doesn't conform to its format, or a JSON resource cannot be decoded. */ public class Decoding(cause: Error) : diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt index 193a6da3fd..ccc54e2490 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt @@ -13,7 +13,7 @@ import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceFactory /** - * Creates [FileResource]s from Urls. + * Creates [FileResource] instances granting access to `file://` URLs stored on the file system. */ public class FileResourceFactory : ResourceFactory { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt index 1088b8ca44..20b5fb79cf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt @@ -13,6 +13,7 @@ import java.net.URLEncoder import kotlin.time.Duration import org.readium.r2.shared.extensions.toMutable import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.toAbsoluteUrl import org.readium.r2.shared.util.toUri /** @@ -73,7 +74,7 @@ public class HttpRequest( } public class Builder( - public val url: AbsoluteUrl, + url: AbsoluteUrl, public var method: Method = Method.GET, public var headers: MutableMap> = mutableMapOf(), public var body: Body? = null, @@ -83,6 +84,10 @@ public class HttpRequest( public var allowUserInteraction: Boolean = false ) { + public var url: AbsoluteUrl + get() = checkNotNull(uriBuilder.build().toAbsoluteUrl()) + set(value) { uriBuilder = value.toUri().buildUpon() } + private var uriBuilder: Uri.Builder = url.toUri().buildUpon() public fun appendQueryParameter(key: String, value: String?): Builder { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt index f5e556fee5..2081db285e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt @@ -13,7 +13,7 @@ import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceFactory /** - * Creates [HttpResource]s. + * Creates [HttpResource] instances granting access to `http://` URLs using an [HttpClient]. */ public class HttpResourceFactory( private val httpClient: HttpClient diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt index a7bad23c64..72f618f91c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt @@ -206,10 +206,8 @@ public class MediaType private constructor( ) /** Returns whether this media type is of a publication file. */ - public val isPublication: Boolean get() = matchesAny( - READIUM_AUDIOBOOK, READIUM_AUDIOBOOK_MANIFEST, CBZ, DIVINA, DIVINA_MANIFEST, EPUB, LCP_PROTECTED_AUDIOBOOK, - LCP_PROTECTED_PDF, LPF, PDF, W3C_WPUB_MANIFEST, READIUM_WEBPUB, READIUM_WEBPUB_MANIFEST, ZAB - ) + public val isPublication: Boolean get() = + matchesAny(CBZ, EPUB, LPF, PDF, W3C_WPUB_MANIFEST, ZAB) || isRwpm || isRpf @Deprecated( "Format and MediaType got merged together", diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt index 5dbf5e1f54..61638d68df 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/Resource.kt @@ -63,6 +63,9 @@ public class FailureResource( /** * Returns a new [Resource] accessing the same data but not owning them. + * + * This is useful when you want to pass a [Resource] to a component which might close it, but you + * want to keep using it after. */ public fun Resource.borrow(): Resource = BorrowedResource(this) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 81de16a61d..433ee2bf7f 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -158,7 +158,7 @@ public class PublicationFactory( * It can be used to modify the manifest, the root container or the list of service * factories of the [Publication]. * @param warnings Logger used to broadcast non-fatal parsing warnings. - * @return A [Publication] or a [OpenError] in case of failure. + * @return A [Publication] or an [OpenError] in case of failure. */ public suspend fun open( asset: Asset, diff --git a/test-app/src/main/assets/readium/error.xhtml b/test-app/src/main/assets/readium/error.xhtml deleted file mode 100644 index 18d09b5d03..0000000000 --- a/test-app/src/main/assets/readium/error.xhtml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - -

Error

- - diff --git a/test-app/src/main/assets/readium/webview_error_icon.svg b/test-app/src/main/assets/readium/webview_error_icon.svg deleted file mode 100644 index b518ba004d..0000000000 --- a/test-app/src/main/assets/readium/webview_error_icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From e22565965467153445b3622a9e712a9e4e4eb1a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Menu?= Date: Mon, 11 Dec 2023 10:23:44 +0100 Subject: [PATCH 70/86] Keep `HttpRequest.Builder.url` immutable --- .../java/org/readium/r2/shared/util/http/HttpRequest.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt index 20b5fb79cf..1088b8ca44 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpRequest.kt @@ -13,7 +13,6 @@ import java.net.URLEncoder import kotlin.time.Duration import org.readium.r2.shared.extensions.toMutable import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.toAbsoluteUrl import org.readium.r2.shared.util.toUri /** @@ -74,7 +73,7 @@ public class HttpRequest( } public class Builder( - url: AbsoluteUrl, + public val url: AbsoluteUrl, public var method: Method = Method.GET, public var headers: MutableMap> = mutableMapOf(), public var body: Body? = null, @@ -84,10 +83,6 @@ public class HttpRequest( public var allowUserInteraction: Boolean = false ) { - public var url: AbsoluteUrl - get() = checkNotNull(uriBuilder.build().toAbsoluteUrl()) - set(value) { uriBuilder = value.toUri().buildUpon() } - private var uriBuilder: Uri.Builder = url.toUri().buildUpon() public fun appendQueryParameter(key: String, value: String?): Builder { From c1db83a9e493c8648e3d1394adc63da231c38e8a Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Sun, 10 Dec 2023 15:18:10 +0100 Subject: [PATCH 71/86] WIP --- .../readium/r2/lcp/LcpContentProtection.kt | 35 +- .../readium/r2/lcp/LcpPublicationRetriever.kt | 43 +- .../java/org/readium/r2/lcp/LcpService.kt | 22 +- .../lcp/license/container/LicenseContainer.kt | 24 +- .../readium/r2/lcp/service/LicensesService.kt | 48 +- .../readium/r2/lcp/service/NetworkService.kt | 24 +- .../AdeptFallbackContentProtection.kt | 53 +- .../protection/ContentProtection.kt | 20 +- .../ContentProtectionSchemeRetriever.kt | 49 - .../LcpFallbackContentProtection.kt | 93 +- .../java/org/readium/r2/shared/util/Either.kt | 2 +- .../r2/shared/util/archive/ArchiveFactory.kt | 73 -- .../util/archive/RecursiveArchiveFactory.kt | 42 - .../r2/shared/util/asset/ArchiveOpener.kt | 95 ++ .../org/readium/r2/shared/util/asset/Asset.kt | 14 +- .../r2/shared/util/asset/AssetOpener.kt | 128 +++ .../r2/shared/util/asset/AssetRetriever.kt | 146 --- .../r2/shared/util/asset/AssetSniffer.kt | 117 +++ .../r2/shared/util/asset/SniffError.kt | 22 + .../util/content/ContentResourceFactory.kt | 4 +- .../readium/r2/shared/util/data/Container.kt | 5 - .../android/AndroidDownloadManager.kt | 23 +- .../shared/util/file/FileResourceFactory.kt | 6 +- .../readium/r2/shared/util/format/Format.kt | 72 ++ .../r2/shared/util/format/FormatRegistry.kt | 56 + .../shared/util/http/HttpResourceFactory.kt | 4 +- .../util/mediatype/DefaultMediaTypeSniffer.kt | 50 - .../shared/util/mediatype/FormatRegistry.kt | 98 -- .../r2/shared/util/mediatype/MediaType.kt | 2 +- .../util/mediatype/MediaTypeRetriever.kt | 184 ---- .../shared/util/mediatype/MediaTypeSniffer.kt | 894 ---------------- .../ArchiveProperties.kt | 3 +- .../shared/util/resource/ResourceFactory.kt | 10 +- .../r2/shared/util/sniff/ContentSniffer.kt | 122 +++ .../r2/shared/util/sniff/DefaultSniffers.kt | 983 ++++++++++++++++++ .../r2/shared/util/sniff/FormatHints.kt | 83 ++ .../shared/util/zip/FileZipArchiveProvider.kt | 41 +- .../r2/shared/util/zip/FileZipContainer.kt | 7 +- .../util/zip/StreamingZipArchiveProvider.kt | 39 +- .../shared/util/zip/StreamingZipContainer.kt | 7 +- .../r2/shared/util/zip/ZipArchiveFactory.kt | 29 - .../r2/shared/util/zip/ZipArchiveOpener.kt | 38 + .../r2/shared/util/zip/ZipMediaTypeSniffer.kt | 42 - .../util/mediatype/FormatRegistryTest.kt | 6 +- .../util/mediatype/MediaTypeRetrieverTest.kt | 8 +- .../r2/shared/util/resource/PropertiesTest.kt | 2 - .../shared/util/resource/ZipContainerTest.kt | 6 +- .../readium/r2/streamer/ParserAssetFactory.kt | 16 +- .../readium/r2/streamer/PublicationFactory.kt | 152 +-- .../r2/streamer/parser/PublicationParser.kt | 9 +- .../r2/streamer/parser/audio/AudioParser.kt | 21 +- .../r2/streamer/parser/epub/EpubParser.kt | 3 +- .../parser/epub/EpubPositionsService.kt | 2 +- .../r2/streamer/parser/image/ImageParser.kt | 20 +- .../r2/streamer/parser/pdf/PdfParser.kt | 3 +- .../parser/readium/LcpdfPositionsService.kt | 2 +- .../parser/readium/ReadiumWebPubParser.kt | 23 +- .../parser/epub/EpubPositionsServiceTest.kt | 4 +- .../streamer/parser/image/ImageParserTest.kt | 30 +- .../org/readium/r2/testapp/Application.kt | 6 +- .../java/org/readium/r2/testapp/Readium.kt | 45 +- .../readium/r2/testapp/data/BookRepository.kt | 6 +- .../org/readium/r2/testapp/data/model/Book.kt | 18 +- .../readium/r2/testapp/domain/Bookshelf.kt | 38 +- .../readium/r2/testapp/domain/ImportError.kt | 5 + .../r2/testapp/domain/PublicationError.kt | 38 +- .../r2/testapp/domain/PublicationRetriever.kt | 25 +- .../readium/r2/testapp/reader/OpeningError.kt | 5 + .../r2/testapp/reader/ReaderRepository.kt | 15 +- test-app/src/main/res/values/strings.xml | 6 +- 70 files changed, 2101 insertions(+), 2265 deletions(-) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/ArchiveOpener.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/SniffError.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/{archive => resource}/ArchiveProperties.kt (95%) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/sniff/ContentSniffer.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/sniff/DefaultSniffers.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/sniff/FormatHints.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index 917d0e5e7c..e3759f06b4 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -17,33 +17,40 @@ import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.ThrowableError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.AssetOpener import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.TransformingContainer internal class LcpContentProtection( private val lcpService: LcpService, private val authentication: LcpAuthenticating, - private val assetRetriever: AssetRetriever + private val assetOpener: AssetOpener ) : ContentProtection { override val scheme: ContentProtection.Scheme = ContentProtection.Scheme.Lcp - override suspend fun supports( - asset: Asset - ): Try = - Try.success(lcpService.isLcpProtected(asset)) - override suspend fun open( asset: Asset, credentials: String?, allowUserInteraction: Boolean ): Try { + if ( + !asset.format.conformsTo(Format.EPUB_LCP) && + !asset.format.conformsTo(Format.RPF_LCP) && + !asset.format.conformsTo(Format.RPF_AUDIO_LCP) && + !asset.format.conformsTo(Format.RPF_IMAGE_LCP) && + !asset.format.conformsTo(Format.RPF_PDF_LCP) && + !asset.format.conformsTo(Format.LCP_LICENSE_DOCUMENT) + ) { + return Try.failure(ContentProtection.OpenError.AssetNotSupported()) + } + return when (asset) { is ContainerAsset -> openPublication(asset, credentials, allowUserInteraction) is ResourceAsset -> openLicense(asset, credentials, allowUserInteraction) @@ -83,7 +90,7 @@ internal class LcpContentProtection( val container = TransformingContainer(asset.container, decryptor::transform) val protectedFile = ContentProtection.Asset( - mediaType = asset.mediaType, + format = asset.format, container = container, onCreatePublication = { decryptor.encryptionData = (manifest.readingOrder + manifest.resources + manifest.links) @@ -145,14 +152,14 @@ internal class LcpContentProtection( val asset = if (link.mediaType != null) { - assetRetriever.retrieve( + assetOpener.open( url, mediaType = link.mediaType ) .map { it as ContainerAsset } .mapFailure { it.wrap() } } else { - assetRetriever.retrieve(url) + assetOpener.open(url) .mapFailure { it.wrap() } .flatMap { if (it is ContainerAsset) { @@ -172,13 +179,13 @@ internal class LcpContentProtection( return asset.flatMap { createResultAsset(it, license) } } - private fun AssetRetriever.RetrieveError.wrap(): ContentProtection.OpenError = + private fun AssetOpener.OpenError.wrap(): ContentProtection.OpenError = when (this) { - is AssetRetriever.RetrieveError.FormatNotSupported -> + is AssetOpener.OpenError.FormatNotSupported -> ContentProtection.OpenError.AssetNotSupported(this) - is AssetRetriever.RetrieveError.Reading -> + is AssetOpener.OpenError.Reading -> ContentProtection.OpenError.Reading(cause) - is AssetRetriever.RetrieveError.SchemeNotSupported -> + is AssetOpener.OpenError.SchemeNotSupported -> ContentProtection.OpenError.AssetNotSupported(this) } } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt index 6e66e45989..786c1f6d94 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt @@ -15,12 +15,12 @@ import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.ErrorException +import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse -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 +import org.readium.r2.shared.util.sniff.FormatHints /** * Utility to acquire a protected publication from an LCP License Document. @@ -28,7 +28,7 @@ import org.readium.r2.shared.util.mediatype.MediaTypeRetriever public class LcpPublicationRetriever( context: Context, private val downloadManager: DownloadManager, - private val mediaTypeRetriever: MediaTypeRetriever + private val assetSniffer: AssetSniffer, ) { @JvmInline @@ -194,19 +194,20 @@ public class LcpPublicationRetriever( } downloadsRepository.removeDownload(requestId.value) - val mediaTypeWithoutLicense = mediaTypeRetriever.retrieve( - download.file, - MediaTypeHints( - mediaTypes = listOfNotNull( - license.publicationLink.mediaType, - download.mediaType + val formatWithoutLicense = + assetSniffer.sniff( + download.file, + FormatHints( + mediaTypes = listOfNotNull( + license.publicationLink.mediaType, + download.mediaType + ) ) - ) - ).getOrElse { MediaType.EPUB } + ).getOrElse { Format.EPUB } try { // Saves the License Document into the downloaded publication - val container = createLicenseContainer(download.file, mediaTypeWithoutLicense) + val container = createLicenseContainer(download.file, formatWithoutLicense) container.write(license) } catch (e: Exception) { tryOrLog { download.file.delete() } @@ -216,20 +217,21 @@ public class LcpPublicationRetriever( return@launch } - val mediaType = mediaTypeRetriever.retrieve( + val format = assetSniffer.sniff( download.file, - MediaTypeHints( + FormatHints( + format = formatWithoutLicense, mediaTypes = listOfNotNull( license.publicationLink.mediaType, download.mediaType ) ) - ).getOrElse { MediaType.EPUB } + ).getOrElse { formatWithoutLicense } val acquiredPublication = LcpService.AcquiredPublication( localFile = download.file, - suggestedFilename = "${license.id}.${formatRegistry.fileExtension(mediaType) ?: "epub"}", - mediaType = mediaType, + suggestedFilename = "${license.id}.${format.fileExtension}", + format, licenseDocument = license ) @@ -285,4 +287,7 @@ public class LcpPublicationRetriever( listeners.remove(lcpRequestId) } } + + private val Format.fileExtension: String get() = + formatRegistry[this]?.fileExtension?.value ?: "epub" } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt index 7cd9932196..4047af316d 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt @@ -29,10 +29,10 @@ import org.readium.r2.lcp.service.PassphrasesService import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.AssetOpener +import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.downloads.DownloadManager -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.format.Format /** * Service used to acquire and open publications protected with LCP. @@ -92,7 +92,7 @@ public interface LcpService { */ public suspend fun retrieveLicense( file: File, - mediaType: MediaType, + format: Format, authentication: LcpAuthenticating, allowUserInteraction: Boolean ): Try @@ -146,7 +146,7 @@ public interface LcpService { public data class AcquiredPublication( val localFile: File, val suggestedFilename: String, - val mediaType: MediaType, + val format: Format, val licenseDocument: LicenseDocument ) { @Deprecated( @@ -164,8 +164,8 @@ public interface LcpService { */ public operator fun invoke( context: Context, - assetRetriever: AssetRetriever, - mediaTypeRetriever: MediaTypeRetriever, + assetOpener: AssetOpener, + assetSniffer: AssetSniffer, downloadManager: DownloadManager ): LcpService? { if (!LcpClient.isAvailable()) { @@ -176,7 +176,7 @@ public interface LcpService { val deviceRepository = DeviceRepository(db) val passphraseRepository = PassphrasesRepository(db) val licenseRepository = LicensesRepository(db) - val network = NetworkService(mediaTypeRetriever) + val network = NetworkService() val device = DeviceService( repository = deviceRepository, network = network, @@ -191,8 +191,8 @@ public interface LcpService { network = network, passphrases = passphrases, context = context, - assetRetriever = assetRetriever, - mediaTypeRetriever = mediaTypeRetriever, + assetOpener = assetOpener, + assetSniffer = assetSniffer, downloadManager = downloadManager ) } @@ -203,7 +203,7 @@ public interface LcpService { ReplaceWith("LcpService(context, AssetRetriever(), MediaTypeRetriever())"), level = DeprecationLevel.ERROR ) - public fun create(context: Context): LcpService? = throw NotImplementedError() + public fun create(context: Context): LcpService = throw NotImplementedError() } @Deprecated( diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt index 3d8688dd42..a52a019485 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt @@ -19,7 +19,7 @@ import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.resource.Resource private val LICENSE_IN_EPUB = Url("META-INF/license.lcpl")!! @@ -39,11 +39,11 @@ internal interface WritableLicenseContainer : LicenseContainer { internal fun createLicenseContainer( file: File, - mediaType: MediaType + format: Format ): WritableLicenseContainer = - when (mediaType) { - MediaType.EPUB -> FileZipLicenseContainer(file.path, LICENSE_IN_EPUB) - MediaType.LCP_LICENSE_DOCUMENT -> LcplLicenseContainer(file) + when { + format.conformsTo(Format.EPUB) -> FileZipLicenseContainer(file.path, LICENSE_IN_EPUB) + format.conformsTo(Format.LCP_LICENSE_DOCUMENT) -> LcplLicenseContainer(file) // Assuming it's a Readium WebPub package (e.g. audiobook, LCPDF, etc.) as a fallback else -> FileZipLicenseContainer(file.path, LICENSE_IN_RPF) } @@ -53,15 +53,15 @@ internal fun createLicenseContainer( asset: Asset ): LicenseContainer = when (asset) { - is ResourceAsset -> createLicenseContainer(asset.resource, asset.mediaType) - is ContainerAsset -> createLicenseContainer(context, asset.container, asset.mediaType) + is ResourceAsset -> createLicenseContainer(asset.resource, asset.format) + is ContainerAsset -> createLicenseContainer(context, asset.container, asset.format) } internal fun createLicenseContainer( resource: Resource, - mediaType: MediaType + format: Format ): LicenseContainer { - if (mediaType != MediaType.LCP_LICENSE_DOCUMENT) { + if (!format.conformsTo(Format.LCP_LICENSE_DOCUMENT)) { throw LcpException(LcpError.Container.OpenFailed) } @@ -76,10 +76,10 @@ internal fun createLicenseContainer( internal fun createLicenseContainer( context: Context, container: Container, - mediaType: MediaType + format: Format ): LicenseContainer { - val licensePath = when (mediaType) { - MediaType.EPUB -> LICENSE_IN_EPUB + val licensePath = when { + format.conformsTo(Format.EPUB) -> LICENSE_IN_EPUB // Assuming it's a Readium WebPub package (e.g. audiobook, LCPDF, etc.) as a fallback else -> LICENSE_IN_RPF } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index dd3c6445ab..90d49c05e0 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -36,14 +36,15 @@ import org.readium.r2.shared.extensions.tryOrLog import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.AssetOpener +import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.mediatype.FormatRegistry -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever +import org.readium.r2.shared.util.sniff.FormatHints import timber.log.Timber internal class LicensesService( @@ -53,13 +54,15 @@ internal class LicensesService( private val network: NetworkService, private val passphrases: PassphrasesService, private val context: Context, - private val assetRetriever: AssetRetriever, - private val mediaTypeRetriever: MediaTypeRetriever, + private val assetOpener: AssetOpener, + private val assetSniffer: AssetSniffer, private val downloadManager: DownloadManager ) : LcpService, CoroutineScope by MainScope() { + private val formatRegistry = FormatRegistry() + override suspend fun isLcpProtected(file: File): Boolean { - val asset = assetRetriever.retrieve(file) + val asset = assetOpener.open(file) .getOrElse { return false } return isLcpProtected(asset) } @@ -68,9 +71,9 @@ internal class LicensesService( tryOr(false) { when (asset) { is ResourceAsset -> - asset.mediaType == MediaType.LCP_LICENSE_DOCUMENT + asset.format.conformsTo(Format.LCP_LICENSE_DOCUMENT) is ContainerAsset -> { - createLicenseContainer(context, asset.container, asset.mediaType).read() + createLicenseContainer(context, asset.container, asset.format).read() true } } @@ -79,13 +82,13 @@ internal class LicensesService( override fun contentProtection( authentication: LcpAuthenticating ): ContentProtection = - LcpContentProtection(this, authentication, assetRetriever) + LcpContentProtection(this, authentication, assetOpener) override fun publicationRetriever(): LcpPublicationRetriever { return LcpPublicationRetriever( context, downloadManager, - mediaTypeRetriever + assetSniffer ) } @@ -105,12 +108,12 @@ internal class LicensesService( override suspend fun retrieveLicense( file: File, - mediaType: MediaType, + format: Format, authentication: LcpAuthenticating, allowUserInteraction: Boolean ): Try = try { - val container = createLicenseContainer(file, mediaType) + val container = createLicenseContainer(file, format) val license = retrieveLicense( container, authentication, @@ -255,16 +258,21 @@ internal class LicensesService( } Timber.i("LCP destination $destination") - val mediaType = network.download( + val mediaTypeHint = network.download( link.url(), destination, mediaType = link.mediaType, onProgress = onProgress - ) ?: link.mediaType ?: MediaType.EPUB + ) + + val format = assetSniffer.sniff( + destination, + FormatHints(mediaTypes = listOfNotNull(mediaTypeHint, link.mediaType)) + ).getOrElse { Format.EPUB } try { // Saves the License Document into the downloaded publication - val container = createLicenseContainer(destination, mediaType) + val container = createLicenseContainer(destination, format) container.write(license) } catch (e: Exception) { tryOrLog { destination.delete() } @@ -273,12 +281,12 @@ internal class LicensesService( return LcpService.AcquiredPublication( localFile = destination, - suggestedFilename = "${license.id}.${mediaType.fileExtension}", - mediaType = mediaType, + suggestedFilename = "${license.id}.${format.fileExtension}", + format = format, licenseDocument = license ) } - private val MediaType.fileExtension: String get() = - FormatRegistry().fileExtension(this) ?: "epub" + private val Format.fileExtension: String get() = + formatRegistry[this]?.fileExtension?.value ?: "epub" } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt index eff6f270f0..a41116f05c 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/NetworkService.kt @@ -23,13 +23,7 @@ import org.readium.r2.lcp.LcpError import org.readium.r2.lcp.LcpException import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.ReadException -import org.readium.r2.shared.util.getOrElse -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 org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import timber.log.Timber internal typealias URLParameters = Map @@ -39,9 +33,7 @@ internal class NetworkException(val status: Int?, cause: Throwable? = null) : Ex cause ) -internal class NetworkService( - private val mediaTypeRetriever: MediaTypeRetriever -) { +internal class NetworkService { enum class Method(val value: String) { GET("GET"), POST("POST"), PUT("PUT"); @@ -142,17 +134,9 @@ internal class NetworkService( } } - mediaTypeRetriever.retrieve( - destination, - MediaTypeHints(connection, mediaType = mediaType.toString()) - ).getOrElse { - when (it) { - is MediaTypeSnifferError.NotRecognized -> - null - is MediaTypeSnifferError.Reading -> - throw ReadException(it.cause) - } - } + connection.contentType + ?.let { MediaType(it) } + ?: mediaType } catch (e: Exception) { Timber.e(e) throw LcpException(LcpError.Network(e)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt index 21b48a6f96..2034c8ed0b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt @@ -9,15 +9,10 @@ package org.readium.r2.shared.publication.protection import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.protection.ContentProtection.Scheme import org.readium.r2.shared.publication.services.contentProtectionServiceFactory -import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.ContainerAsset -import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.decodeXml -import org.readium.r2.shared.util.data.readDecodeOrElse -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.format.Format /** * [ContentProtection] implementation used as a fallback by the Streamer to detect Adept DRM, @@ -28,29 +23,17 @@ public class AdeptFallbackContentProtection : ContentProtection { override val scheme: Scheme = Scheme.Adept - override suspend fun supports(asset: Asset): Try { - if (asset !is ContainerAsset) { - return Try.success(false) - } - - return isAdept(asset) - } - override suspend fun open( asset: Asset, credentials: String?, allowUserInteraction: Boolean ): Try { - if (asset !is ContainerAsset) { - return Try.failure( - ContentProtection.OpenError.AssetNotSupported( - DebugError("A container asset was expected.") - ) - ) + if (asset !is ContainerAsset || !asset.format.conformsTo(Format.EPUB_ADEPT)) { + return Try.failure(ContentProtection.OpenError.AssetNotSupported()) } val protectedFile = ContentProtection.Asset( - asset.mediaType, + asset.format, asset.container, onCreatePublication = { servicesBuilder.contentProtectionServiceFactory = @@ -60,32 +43,4 @@ public class AdeptFallbackContentProtection : ContentProtection { return Try.success(protectedFile) } - - private suspend fun isAdept(asset: ContainerAsset): Try { - if (!asset.mediaType.matches(MediaType.EPUB)) { - return Try.success(false) - } - - asset.container[Url("META-INF/encryption.xml")!!] - ?.readDecodeOrElse( - decode = { it.decodeXml() }, - recoverRead = { return Try.success(false) }, - recoverDecode = { return Try.success(false) } - ) - ?.get("EncryptedData", EpubEncryption.ENC) - ?.flatMap { it.get("KeyInfo", EpubEncryption.SIG) } - ?.flatMap { it.get("resource", "http://ns.adobe.com/adept") } - ?.takeIf { it.isNotEmpty() } - ?.let { return Try.success(true) } - - return asset.container[Url("META-INF/rights.xml")!!] - ?.readDecodeOrElse( - decode = { it.decodeXml() }, - recoverRead = { return Try.success(false) }, - recoverDecode = { return Try.success(false) } - ) - ?.takeIf { it.namespace == "http://ns.adobe.com/adept" } - ?.let { Try.success(true) } - ?: Try.success(false) - } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index a9d41be888..6c63ec064e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -9,11 +9,6 @@ package org.readium.r2.shared.publication.protection -import kotlin.Boolean -import kotlin.Deprecated -import kotlin.DeprecationLevel -import kotlin.String -import kotlin.Unit import org.readium.r2.shared.publication.LocalizedString import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.ContentProtectionService @@ -21,7 +16,7 @@ import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.resource.Resource /** @@ -43,19 +38,12 @@ public interface ContentProtection { ) : OpenError("An error occurred while trying to read asset.", cause) public class AssetNotSupported( - override val cause: Error? + override val cause: Error? = null ) : OpenError("Asset is not supported.", cause) } public val scheme: Scheme - /** - * Returns if this [ContentProtection] supports the given [asset]. - */ - public suspend fun supports( - asset: org.readium.r2.shared.util.asset.Asset - ): Try - /** * Attempts to unlock a potentially protected publication asset. * @@ -71,14 +59,14 @@ public interface ContentProtection { /** * Holds the result of opening an [Asset] with a [ContentProtection]. * - * @property mediaType Media type of the asset + * @property format Format of the asset * @property container Container to access the publication through * @property onCreatePublication Called on every parsed Publication.Builder * It can be used to modify the `Manifest`, the root [Container] or the list of service * factories of a [Publication]. */ public data class Asset( - val mediaType: MediaType, + val format: Format, val container: Container, val onCreatePublication: Publication.Builder.() -> Unit = {} ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt deleted file mode 100644 index 07c54529f5..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtectionSchemeRetriever.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.publication.protection - -import kotlin.String -import kotlin.let -import kotlin.takeIf -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.getOrElse - -/** - * Retrieves [ContentProtection] schemes of assets. - */ -public class ContentProtectionSchemeRetriever( - contentProtections: List -) { - private val contentProtections: List = - contentProtections + listOf( - LcpFallbackContentProtection(), - AdeptFallbackContentProtection() - ) - - public sealed class Error( - override val message: String, - override val cause: org.readium.r2.shared.util.Error? - ) : org.readium.r2.shared.util.Error { - - public object NotRecognized : - Error("No content protection recognized the given asset.", null) - - public class Reading(override val cause: org.readium.r2.shared.util.data.ReadError) : - Error("An error occurred while trying to read asset.", cause) - } - - public suspend fun retrieve(asset: org.readium.r2.shared.util.asset.Asset): Try { - for (protection in contentProtections) { - protection.supports(asset) - .getOrElse { return Try.failure(Error.Reading(it)) } - .takeIf { it } - ?.let { return Try.success(protection.scheme) } - } - - return Try.failure(Error.NotRecognized) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index ae52b1cf04..f7b1459329 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -7,22 +7,12 @@ package org.readium.r2.shared.publication.protection import org.readium.r2.shared.InternalReadiumApi -import org.readium.r2.shared.publication.encryption.encryption import org.readium.r2.shared.publication.protection.ContentProtection.Scheme import org.readium.r2.shared.publication.services.contentProtectionServiceFactory -import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.ContainerAsset -import org.readium.r2.shared.util.asset.ResourceAsset -import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.decodeRwpm -import org.readium.r2.shared.util.data.decodeXml -import org.readium.r2.shared.util.data.readDecodeOrElse -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.format.Format /** * [ContentProtection] implementation used as a fallback by the Streamer to detect LCP DRM @@ -34,33 +24,28 @@ public class LcpFallbackContentProtection : ContentProtection { override val scheme: Scheme = Scheme.Lcp - override suspend fun supports(asset: Asset): Try = - when (asset) { - is ContainerAsset -> isLcpProtected( - asset.container, - asset.mediaType - ) - is ResourceAsset -> - Try.success( - asset.mediaType.matches(MediaType.LCP_LICENSE_DOCUMENT) - ) - } - override suspend fun open( asset: Asset, credentials: String?, allowUserInteraction: Boolean ): Try { + if ( + !asset.format.conformsTo(Format.EPUB_LCP) && + !asset.format.conformsTo(Format.RPF_LCP) && + !asset.format.conformsTo(Format.RPF_AUDIO_LCP) && + !asset.format.conformsTo(Format.RPF_IMAGE_LCP) && + !asset.format.conformsTo(Format.RPF_PDF_LCP) + ) { + return Try.failure(ContentProtection.OpenError.AssetNotSupported()) + } + if (asset !is ContainerAsset) { - return Try.failure( - ContentProtection.OpenError.AssetNotSupported( - DebugError("A container asset was expected.") - ) + return Try.failure(ContentProtection.OpenError.AssetNotSupported() ) } val protectedFile = ContentProtection.Asset( - asset.mediaType, + asset.format, asset.container, onCreatePublication = { servicesBuilder.contentProtectionServiceFactory = @@ -70,56 +55,4 @@ public class LcpFallbackContentProtection : ContentProtection { return Try.success(protectedFile) } - - private suspend fun isLcpProtected(container: Container, mediaType: MediaType): Try { - val isRpf = mediaType.isRpf - val isEpub = mediaType.matches(MediaType.EPUB) - - if (!isRpf && !isEpub) { - return Try.success(false) - } - - val licenseUrl = when { - isRpf -> Url("license.lcpl")!! - else -> Url("META-INF/license.lcpl")!! // isEpub - } - container[licenseUrl] - ?.let { return Try.success(true) } - - return when { - isRpf -> hasLcpSchemeInManifest(container) - else -> hasLcpSchemeInEncryptionXml(container) // isEpub - } - } - - private suspend fun hasLcpSchemeInManifest(container: Container): Try { - val manifest = container[Url("manifest.json")!!] - ?.readDecodeOrElse( - decode = { it.decodeRwpm() }, - recoverRead = { return Try.success(false) }, - recoverDecode = { return Try.success(false) } - ) ?: return Try.success(false) - - val manifestHasLcpScheme = manifest - .readingOrder - .any { it.properties.encryption?.scheme == "http://readium.org/2014/01/lcp" } - - return Try.success(manifestHasLcpScheme) - } - - private suspend fun hasLcpSchemeInEncryptionXml(container: Container): Try { - val encryptionXml = container[Url("META-INF/encryption.xml")!!] - ?.readDecodeOrElse( - decode = { it.decodeXml() }, - recover = { return Try.failure(it) } - ) ?: return Try.success(false) - - val hasLcpScheme = encryptionXml - .get("EncryptedData", EpubEncryption.ENC) - .flatMap { it.get("KeyInfo", EpubEncryption.SIG) } - .flatMap { it.get("RetrievalMethod", EpubEncryption.SIG) } - .any { it.getAttr("URI") == "license.lcpl#/encryption/content_key" } - - return Try.success(hasLcpScheme) - } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Either.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Either.kt index 8d2eed5693..f51e34f319 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Either.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Either.kt @@ -9,7 +9,7 @@ package org.readium.r2.shared.util /** * Generic wrapper to store two mutually exclusive types. */ -public sealed class Either { +public sealed class Either { public data class Left(val value: A) : Either() public data class Right(val value: B) : Either() diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt deleted file mode 100644 index ef7572a35d..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveFactory.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.archive - -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.Readable -import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Resource - -/** - * A factory to create [Container]s from archive [Resource]s. - */ -public interface ArchiveFactory { - - public sealed class CreateError( - override val message: String, - override val cause: org.readium.r2.shared.util.Error? - ) : org.readium.r2.shared.util.Error { - - public class FormatNotSupported( - public val mediaType: MediaType, - cause: org.readium.r2.shared.util.Error? = null - ) : CreateError("Media type not supported.", cause) - - public class Reading( - override val cause: org.readium.r2.shared.util.data.ReadError - ) : CreateError("An error occurred while attempting to read the resource.", cause) - } - - /** - * Creates a new [Container] to access the entries of the given archive. - */ - public suspend fun create( - mediaType: MediaType, - source: Readable - ): Try, CreateError> -} - -/** - * A composite [ArchiveFactory] which tries several factories until it finds one which supports - * the format. -*/ -public class CompositeArchiveFactory( - private val factories: List -) : ArchiveFactory { - - public constructor(vararg factories: ArchiveFactory) : - this(factories.toList()) - - override suspend fun create( - mediaType: MediaType, - source: Readable - ): Try, ArchiveFactory.CreateError> { - for (factory in factories) { - factory.create(mediaType, source) - .getOrElse { error -> - when (error) { - is ArchiveFactory.CreateError.FormatNotSupported -> null - else -> return Try.failure(error) - } - } - ?.let { return Try.success(it) } - } - - return Try.failure(ArchiveFactory.CreateError.FormatNotSupported(mediaType)) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt deleted file mode 100644 index 419add010e..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/RecursiveArchiveFactory.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.archive - -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.Readable -import org.readium.r2.shared.util.mediatype.FormatRegistry -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.tryRecover - -/** - * Decorates an [ArchiveFactory] to accept media types that [formatRegistry] claims to be - * subtypes of the one given in [create]. - */ -internal class RecursiveArchiveFactory( - private val archiveFactory: ArchiveFactory, - private val formatRegistry: FormatRegistry -) : ArchiveFactory { - - override suspend fun create( - mediaType: MediaType, - source: Readable - ): Try, ArchiveFactory.CreateError> = - archiveFactory.create(mediaType, source) - .tryRecover { error -> - when (error) { - is ArchiveFactory.CreateError.FormatNotSupported -> { - formatRegistry.superType(mediaType) - ?.let { create(it, source) } - ?: Try.failure(error) - } - is ArchiveFactory.CreateError.Reading -> - Try.failure(error) - } - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ArchiveOpener.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ArchiveOpener.kt new file mode 100644 index 0000000000..273f4d445e --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ArchiveOpener.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.asset + +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.resource.Resource + +/** + * A factory to create [Container]s from archive [Resource]s. + */ +public interface ArchiveOpener { + + public sealed class OpenError( + override val message: String, + override val cause: org.readium.r2.shared.util.Error? + ) : org.readium.r2.shared.util.Error { + + public class FormatNotSupported( + public val format: Format, + cause: org.readium.r2.shared.util.Error? = null + ) : OpenError("Format not supported.", cause) + + public class Reading( + override val cause: org.readium.r2.shared.util.data.ReadError + ) : OpenError("An error occurred while attempting to read the resource.", cause) + } + + /** + * Creates a new [Container] to access the entries of the given archive. + */ + public suspend fun open( + format: Format, + source: Readable + ): Try, OpenError> + + /** + * Creates a new [Container] to access the entries of the given archive. + */ + public suspend fun sniffOpen( + source: Readable + ): Try +} + +/** + * A composite [ArchiveOpener] which tries several factories until it finds one which supports + * the format. +*/ +public class CompositeArchiveOpener( + private val factories: List +) : ArchiveOpener { + + public constructor(vararg factories: ArchiveOpener) : + this(factories.toList()) + + override suspend fun open( + format: Format, + source: Readable + ): Try, ArchiveOpener.OpenError> { + for (factory in factories) { + factory.open(format, source) + .getOrElse { error -> + when (error) { + is ArchiveOpener.OpenError.FormatNotSupported -> null + else -> return Try.failure(error) + } + } + ?.let { return Try.success(it) } + } + + return Try.failure(ArchiveOpener.OpenError.FormatNotSupported(format)) + } + + override suspend fun sniffOpen(source: Readable): Try { + for (factory in factories) { + factory.sniffOpen(source) + .getOrElse { error -> + when (error) { + is SniffError.NotRecognized -> null + else -> return Try.failure(error) + } + } + ?.let { return Try.success(it) } + } + + return Try.failure(SniffError.NotRecognized) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt index 45bef416fe..19e56bfc5d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt @@ -7,7 +7,7 @@ package org.readium.r2.shared.util.asset import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.resource.Resource /** @@ -16,9 +16,9 @@ import org.readium.r2.shared.util.resource.Resource public sealed class Asset { /** - * Media type of the asset. + * Format of the asset. */ - public abstract val mediaType: MediaType + public abstract val format: Format /** * Releases in-memory resources related to this asset. @@ -29,11 +29,11 @@ public sealed class Asset { /** * A container asset providing access to several resources. * - * @param mediaType Media type of the asset. + * @param format Format of the asset. * @param container Opened container to access asset resources. */ public class ContainerAsset( - override val mediaType: MediaType, + override val format: Format, public val container: Container ) : Asset() { @@ -45,11 +45,11 @@ public class ContainerAsset( /** * A single resource asset. * - * @param mediaType Media type of the asset. + * @param format Format of the asset. * @param resource Opened resource to access the asset. */ public class ResourceAsset( - override val mediaType: MediaType, + override val format: Format, public val resource: Resource ) : Asset() { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt new file mode 100644 index 0000000000..b33ace9bc6 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.asset + +import java.io.File +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.ResourceFactory +import org.readium.r2.shared.util.sniff.FormatHints +import org.readium.r2.shared.util.toUrl +import timber.log.Timber + +/** + * Retrieves an [Asset] instance providing reading access to the resource(s) of an asset stored at + * a given [Url] as well as a canonical media type. + */ +public class AssetOpener( + private val assetSniffer: AssetSniffer, + private val resourceFactory: ResourceFactory, + private val archiveOpener: ArchiveOpener +) { + + public sealed class OpenError( + override val message: String, + override val cause: Error? + ) : Error { + + /** + * The scheme (e.g. http, file, content) for the requested [Url] is not supported by the + * [resourceFactory]. + */ + public class SchemeNotSupported( + public val scheme: Url.Scheme, + cause: Error? = null + ) : OpenError("Url scheme $scheme is not supported.", cause) + + /** + * The format of the resource at the requested [Url] is not recognized by the + * [assetSniffer]. + */ + public class FormatNotSupported( + cause: Error? = null + ) : OpenError("Asset format is not supported.", cause) + + /** + * An error occurred when trying to read the asset. + */ + public class Reading(override val cause: org.readium.r2.shared.util.data.ReadError) : + OpenError("An error occurred when trying to read asset.", cause) + } + + /** + * Retrieves an asset from an url and a known format. + */ + public suspend fun open( + url: AbsoluteUrl, + format: Format + ): Try { + val resource = retrieveResource(url) + .getOrElse { return Try.failure(it) } + + val archive = archiveOpener.open(format, resource) + .getOrElse { + return when (it) { + is ArchiveOpener.OpenError.Reading -> + Try.failure(OpenError.Reading(it.cause)) + is ArchiveOpener.OpenError.FormatNotSupported -> + Try.success(ResourceAsset(format, resource)) + } + } + + return Try.success(ContainerAsset(format, archive)) + } + + private suspend fun retrieveResource( + url: AbsoluteUrl + ): Try { + return resourceFactory.create(url) + .mapFailure { error -> + when (error) { + is ResourceFactory.Error.SchemeNotSupported -> + OpenError.SchemeNotSupported(error.scheme, error) + } + } + } + + /* Sniff unknown assets */ + + /** + * Retrieves an asset from an unknown local file. + */ + public suspend fun open(file: File, mediaType: MediaType? = null): Try = + open(file.toUrl(), mediaType) + + /** + * Retrieves an asset from an unknown [AbsoluteUrl]. + */ + public suspend fun open(url: AbsoluteUrl, mediaType: MediaType? = null): Try { + val resource = resourceFactory.create(url) + .getOrElse { + return Try.failure( + when (it) { + is ResourceFactory.Error.SchemeNotSupported -> + OpenError.SchemeNotSupported(it.scheme) + } + ) + } + + Timber.d("sniffing asset") + return assetSniffer.sniffOpen(resource, FormatHints(mediaType = mediaType)) + .mapFailure { + when (it) { + SniffError.NotRecognized -> OpenError.FormatNotSupported() + is SniffError.Reading -> OpenError.Reading(it.cause) + } + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt deleted file mode 100644 index e9f37bf9e6..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.asset - -import java.io.File -import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.DebugError -import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.archive.ArchiveFactory -import org.readium.r2.shared.util.archive.RecursiveArchiveFactory -import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.mediatype.FormatRegistry -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.ResourceFactory -import org.readium.r2.shared.util.toUrl - -/** - * Retrieves an [Asset] instance providing reading access to the resource(s) of an asset stored at - * a given [Url] as well as a canonical media type. - */ -public class AssetRetriever( - private val mediaTypeRetriever: MediaTypeRetriever, - private val resourceFactory: ResourceFactory, - archiveFactory: ArchiveFactory, - formatRegistry: FormatRegistry -) { - - public sealed class RetrieveError( - override val message: String, - override val cause: Error? - ) : Error { - - /** - * The scheme (e.g. http, file, content) for the requested [Url] is not supported by the - * [resourceFactory]. - */ - public class SchemeNotSupported( - public val scheme: Url.Scheme, - cause: Error? = null - ) : RetrieveError("Url scheme $scheme is not supported.", cause) - - /** - * The format of the resource at the requested [Url] is not recognized by the - * [mediaTypeRetriever] and [archiveFactory]. - */ - public class FormatNotSupported(cause: Error) : - RetrieveError("Asset format is not supported.", cause) - - /** - * An error occurred when trying to read the asset. - */ - public class Reading(override val cause: org.readium.r2.shared.util.data.ReadError) : - RetrieveError("An error occurred when trying to read asset.", cause) - } - - private val archiveFactory: ArchiveFactory = - RecursiveArchiveFactory(archiveFactory, formatRegistry) - - /** - * Retrieves an asset from an url and a known media type. - */ - public suspend fun retrieve( - url: AbsoluteUrl, - mediaType: MediaType - ): Try { - val resource = retrieveResource(url, mediaType) - .getOrElse { return Try.failure(it) } - - val archive = archiveFactory.create(mediaType, resource) - .getOrElse { - return when (it) { - is ArchiveFactory.CreateError.Reading -> - Try.failure(RetrieveError.Reading(it.cause)) - is ArchiveFactory.CreateError.FormatNotSupported -> - Try.success(ResourceAsset(mediaType, resource)) - } - } - - return Try.success(ContainerAsset(mediaType, archive)) - } - - private suspend fun retrieveResource( - url: AbsoluteUrl, - mediaType: MediaType - ): Try { - return resourceFactory.create(url, mediaType) - .mapFailure { error -> - when (error) { - is ResourceFactory.Error.SchemeNotSupported -> - RetrieveError.SchemeNotSupported(error.scheme, error) - } - } - } - - /* Sniff unknown assets */ - - /** - * Retrieves an asset from an unknown local file. - */ - public suspend fun retrieve(file: File): Try = - retrieve(file.toUrl()) - - /** - * Retrieves an asset from an unknown [AbsoluteUrl]. - */ - public suspend fun retrieve(url: AbsoluteUrl): Try { - val resource = resourceFactory.create(url) - .getOrElse { - return Try.failure( - when (it) { - is ResourceFactory.Error.SchemeNotSupported -> - RetrieveError.SchemeNotSupported(it.scheme) - } - ) - } - - val mediaType = mediaTypeRetriever.retrieve(resource) - .getOrElse { - return Try.failure( - RetrieveError.FormatNotSupported( - DebugError("Cannot determine asset media type.") - ) - ) - } - - val container = archiveFactory.create(mediaType, resource) - .getOrElse { - when (it) { - is ArchiveFactory.CreateError.Reading -> - return Try.failure(RetrieveError.Reading(it.cause)) - is ArchiveFactory.CreateError.FormatNotSupported -> - return Try.success(ResourceAsset(mediaType, resource)) - } - } - - return Try.success(ContainerAsset(mediaType, container)) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt new file mode 100644 index 0000000000..c583398990 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.asset + +import java.io.File +import org.readium.r2.shared.util.Either +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.file.FileResource +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.borrow +import org.readium.r2.shared.util.sniff.ContentSniffer +import org.readium.r2.shared.util.sniff.FormatHints +import org.readium.r2.shared.util.tryRecover +import org.readium.r2.shared.util.use + +public class AssetSniffer( + private val contentSniffer: ContentSniffer, + private val archiveOpener: ArchiveOpener +) { + public suspend fun sniffOpen( + source: Resource, + hints: FormatHints = FormatHints() + ): Try = + sniff(null, Either.Left(source), hints) + + public suspend fun sniffOpen(file: File, hints: FormatHints): Try = + sniff(null, Either.Left(FileResource(file)), hints) + + public suspend fun sniff( + source: Resource, + hints: FormatHints = FormatHints() + ): Try = + sniffOpen(source.borrow(), hints).map { it.format } + + public suspend fun sniff(file: File, hints: FormatHints): Try = + FileResource(file).use { sniff(it, hints) } + + public suspend fun sniff( + container: Container, + hints: FormatHints = FormatHints() + ): Try = + sniff(null, Either.Right(container), hints).map { it.format } + + private suspend fun sniff( + format: Format?, + source: Either>, + hints: FormatHints + ): Try { + contentSniffer.sniffHints(format, hints) + ?.takeIf { format == null || it.conformsTo(format) } + ?.takeIf { it != format} + ?.let { return sniff(it, source, hints) } + + when (source) { + is Either.Left -> + contentSniffer.sniffBlob(format, source.value) + is Either.Right -> + contentSniffer.sniffContainer(format, source.value) + } + .getOrElse { return Try.failure(SniffError.Reading(it)) } + ?.takeIf { format == null || it.conformsTo(format) } + ?.takeIf { it != format } + ?.let { return sniff(it, source, hints) } + + if (source is Either.Left) { + tryOpenArchive(format, source.value) + .getOrElse { return Try.failure(SniffError.Reading(it)) } + ?.let { return sniff(it.format, Either.Right(it.container), hints) } + } + + format?.let { + val asset = when (source) { + is Either.Left -> ResourceAsset(it, source.value) + is Either.Right -> ContainerAsset(it, source.value) + } + return Try.success(asset) + } + + return Try.failure(SniffError.NotRecognized) + } + + private suspend fun tryOpenArchive( + format: Format?, + source: Readable + ): Try = + if (format == null) { + archiveOpener.sniffOpen(source) + .tryRecover { + when (it) { + is SniffError.NotRecognized -> + Try.success(null) + is SniffError.Reading -> + Try.failure(it.cause) + } + } + } else { + archiveOpener.open(format, source) + .map { ContainerAsset(format, it) } + .tryRecover { + when (it) { + is ArchiveOpener.OpenError.FormatNotSupported -> + Try.success(null) + is ArchiveOpener.OpenError.Reading -> + Try.failure(it.cause) + } + } + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SniffError.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SniffError.kt new file mode 100644 index 0000000000..6539ea17fd --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SniffError.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.asset + +import org.readium.r2.shared.util.Error +import org.readium.r2.shared.util.data.ReadError + +public sealed class SniffError( + override val message: String, + override val cause: Error? +) : Error { + + public data object NotRecognized : + SniffError("Format of resource could not be inferred.", null) + + public data class Reading(override val cause: ReadError) : + SniffError("An error occurred while trying to read content.", cause) +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt index 8ffac52585..3136b17742 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResourceFactory.kt @@ -9,7 +9,6 @@ package org.readium.r2.shared.util.content import android.content.ContentResolver import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceFactory import org.readium.r2.shared.util.toUri @@ -23,8 +22,7 @@ public class ContentResourceFactory( ) : ResourceFactory { override suspend fun create( - url: AbsoluteUrl, - mediaType: MediaType? + url: AbsoluteUrl ): Try { if (!url.isContent) { return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index 503229fe2f..dce2eedbda 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -19,11 +19,6 @@ import org.readium.r2.shared.util.use */ public interface Container : Iterable, SuspendingCloseable { - /** - * Media type of the archive the container offers access to if any. - */ - public val archiveMediaType: MediaType? get() = null - /** * Direct source to this container, when available. */ diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index 758351fd19..04c9f42fd0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -30,10 +30,7 @@ import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.http.HttpError import org.readium.r2.shared.util.http.HttpStatus -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 import org.readium.r2.shared.util.toUri import org.readium.r2.shared.util.units.Hz import org.readium.r2.shared.util.units.hz @@ -43,8 +40,6 @@ import org.readium.r2.shared.util.units.hz */ public class AndroidDownloadManager internal constructor( private val context: Context, - private val mediaTypeRetriever: MediaTypeRetriever, - private val formatRegistry: FormatRegistry, private val destStorage: Storage, private val dirType: String, private val refreshRate: Hz, @@ -59,9 +54,7 @@ public class AndroidDownloadManager internal constructor( * android.permission.DOWNLOAD_WITHOUT_NOTIFICATION. * * @param context Android context - * @param mediaTypeRetriever Retrieves the media type of the download content, if the server * communicates it. - * @param formatRegistry Associates a media type to its file extension. * @param destStorage Location where downloads should be stored * @param refreshRate Frequency with which download status will be checked and * listeners notified @@ -70,15 +63,11 @@ public class AndroidDownloadManager internal constructor( */ public constructor( context: Context, - mediaTypeRetriever: MediaTypeRetriever, - formatRegistry: FormatRegistry = FormatRegistry(), destStorage: Storage = Storage.App, refreshRate: Hz = 60.0.hz, allowDownloadsOverMetered: Boolean = true ) : this( context = context, - mediaTypeRetriever = mediaTypeRetriever, - formatRegistry = formatRegistry, destStorage = destStorage, dirType = Environment.DIRECTORY_DOWNLOADS, refreshRate = refreshRate, @@ -253,7 +242,7 @@ public class AndroidDownloadManager internal constructor( SystemDownloadManager.STATUS_SUCCESSFUL -> { prepareResult( Uri.parse(facade.localUri!!)!!.toFile(), - mediaTypeHint = facade.mediaType?.let { MediaType(it) } + mediaType= facade.mediaType?.let { MediaType(it) } ) .onSuccess { download -> listenersForId.forEach { it.onDownloadCompleted(id, download) } @@ -272,15 +261,9 @@ public class AndroidDownloadManager internal constructor( } } - private suspend fun prepareResult(destFile: File, mediaTypeHint: MediaType?): Try = + private suspend fun prepareResult(destFile: File, mediaType: MediaType?): Try = withContext(Dispatchers.IO) { - val mediaType = mediaTypeRetriever.retrieve( - destFile, - MediaTypeHints(mediaType = mediaTypeHint) - ).getOrNull() - - val extension = mediaType?.let { formatRegistry.fileExtension(it) } - ?: destFile.extension.takeUnless { it.isEmpty() } + val extension = destFile.extension.takeUnless { it.isEmpty() } val newDest = File(destFile.parent, generateFileName(extension)) val renamed = tryOr(false) { destFile.renameTo(newDest) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt index ccc54e2490..bbe39406d7 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/file/FileResourceFactory.kt @@ -8,7 +8,6 @@ package org.readium.r2.shared.util.file import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceFactory @@ -18,13 +17,12 @@ import org.readium.r2.shared.util.resource.ResourceFactory public class FileResourceFactory : ResourceFactory { override suspend fun create( - url: AbsoluteUrl, - mediaType: MediaType? + url: AbsoluteUrl ): Try { val file = url.toFile() ?: return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) - val resource = FileResource(file, mediaType) + val resource = FileResource(file) return Try.success(resource) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt new file mode 100644 index 0000000000..4e59635b67 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.format + +@JvmInline +public value class Format(public val id: String) { + + public fun conformsTo(other: Format): Boolean { + val thisComponents = id.split(".") + val otherComponents = other.id.split(".") + return thisComponents.containsAll(otherComponents) + } + + public companion object { + + public val RAR: Format = Format("rar") + public val CBR: Format = Format("rar.image") + + public val ZIP: Format = Format("zip") + public val CBZ: Format = Format("zip.image") + public val ZAB: Format = Format("zip.audio") + public val LPF: Format = Format("zip.lpf") + public val EPUB: Format = Format("zip.epub") + public val EPUB_LCP: Format = Format("zip.epub.lcp") + public val EPUB_ADEPT: Format = Format("zip.epub.adept") + + public val RPF: Format = Format("zip.rpf") + public val RPF_AUDIO: Format = Format("zip.rpf.audio") + public val RPF_AUDIO_LCP: Format = Format("zip.rpf.audio.lcp") + public val RPF_IMAGE: Format = Format("zip.rpf.image") + public val RPF_IMAGE_LCP: Format = Format("zip.rpf.image.lcp") + public val RPF_PDF: Format = Format("zip.rpf.pdf") + public val RPF_PDF_LCP: Format = Format("zip.rpf.pdf.lcp") + public val RPF_LCP: Format = Format("zip.rpf.lcp") + + public val JSON: Format = Format("json") + public val JSON_PROBLEM_DETAILS: Format = Format("json.problem_details") + public val LCP_LICENSE_DOCUMENT: Format = Format("json.lcpl") + public val W3C_WPUB_MANIFEST: Format = Format("json.w3c_wp_manifest") + public val RWPM: Format = Format("json.rwpm") + public val RWPM_AUDIO: Format = Format("json.rwpm.audio") + public val RWPM_IMAGE: Format = Format("json.rwpm.image") + public val OPDS2: Format = Format("json.opds") + public val OPDS2_PUBLICATION: Format = Format("json.opds_publication") + public val OPDS_AUTHENTICATION: Format = Format("json.opds_authentication") + + public val PDF: Format = Format("pdf") + public val HTML: Format = Format("html") + + public val AVIF: Format = Format("avif") + public val BMP: Format = Format("bmp") + public val GIF: Format = Format("gif") + public val JPEG: Format = Format("jpeg") + public val JXL: Format = Format("jxl") + public val PNG: Format = Format("png") + public val TIFF: Format = Format("tiff") + public val WEBP: Format = Format("webp") + + + public val XML: Format = Format("xml") + public val XHTML: Format = Format("xml.html") + public val ATOM: Format = Format("xml.atom") + public val OPDS1: Format = Format("xml.atom.opds") + public val OPDS1_ENTRY: Format = Format("xml.atom.opds_entry") + public val OPDS1_NAVIGATION_FEED: Format = Format("xml.atom.opds_navigation_feed") + public val OPDS1_ACQUISITION_FEED: Format = Format("xml.atom.opds_acquisition_feed") + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt new file mode 100644 index 0000000000..420269f0b7 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.format + +import org.readium.r2.shared.util.mediatype.MediaType + +@JvmInline +public value class FileExtension( + public val value: String +) +public data class FormatInfo( + public val mediaType: MediaType, + public val fileExtension: FileExtension +) + +/** + * Registry of format metadata (e.g. file extension) associated to canonical media types. + */ +public class FormatRegistry( + formatInfo: Map = mapOf( + Format.CBR to FormatInfo(MediaType.CBR, FileExtension("cbr")), + Format.CBZ to FormatInfo(MediaType.CBZ, FileExtension("cbz")), + Format.RPF_IMAGE to FormatInfo(MediaType.DIVINA, FileExtension("divina")), + Format.RWPM_IMAGE to FormatInfo(MediaType.DIVINA_MANIFEST, FileExtension("json")), + Format.EPUB to FormatInfo(MediaType.EPUB, FileExtension("epub")), + Format.LCP_LICENSE_DOCUMENT to FormatInfo(MediaType.LCP_LICENSE_DOCUMENT, FileExtension("lcpl")), + Format.RPF_AUDIO_LCP to FormatInfo(MediaType.LCP_PROTECTED_AUDIOBOOK, FileExtension("lcpa")), + Format.RPF_PDF_LCP to FormatInfo(MediaType.LCP_PROTECTED_PDF, FileExtension("lcpdf")), + Format.PDF to FormatInfo(MediaType.PDF, FileExtension("pdf")), + Format.RPF_AUDIO to FormatInfo(MediaType.READIUM_AUDIOBOOK, FileExtension("audiobook")), + Format.RWPM_AUDIO to FormatInfo(MediaType.READIUM_AUDIOBOOK_MANIFEST, FileExtension("json")), + Format.RPF to FormatInfo(MediaType.READIUM_WEBPUB, FileExtension("webpub")), + Format.RWPM to FormatInfo(MediaType.READIUM_WEBPUB, FileExtension("json")), + Format.JPEG to FormatInfo(MediaType.JPEG, FileExtension("jpg")) + ) +) { + + private val formatInfo: MutableMap = formatInfo.toMutableMap() + + /** + * Registers format info for the given [Format]. + */ + public fun register( + format: Format, + formatInfo: FormatInfo + ) { + this.formatInfo[format] = formatInfo + } + + public operator fun get(format: Format): FormatInfo? = + formatInfo[format] +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt index 2081db285e..ef787bf800 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/http/HttpResourceFactory.kt @@ -8,7 +8,6 @@ package org.readium.r2.shared.util.http import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceFactory @@ -20,8 +19,7 @@ public class HttpResourceFactory( ) : ResourceFactory { override suspend fun create( - url: AbsoluteUrl, - mediaType: MediaType? + url: AbsoluteUrl ): Try { if (!url.isHttp) { return Try.failure(ResourceFactory.Error.SchemeNotSupported(url.scheme)) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt deleted file mode 100644 index 99c4b368f0..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/DefaultMediaTypeSniffer.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.mediatype - -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.Readable -import org.readium.r2.shared.util.zip.ZipMediaTypeSniffer - -/** - * The default composite sniffer provided by Readium for all known formats. - * The sniffers order is important, because some formats are subsets of other formats. - */ -public class DefaultMediaTypeSniffer : MediaTypeSniffer { - - private val sniffer: MediaTypeSniffer = - CompositeMediaTypeSniffer( - listOf( - WebPubMediaTypeSniffer, - EpubMediaTypeSniffer, - LpfMediaTypeSniffer, - ArchiveMediaTypeSniffer, - PdfMediaTypeSniffer, - BitmapMediaTypeSniffer, - XhtmlMediaTypeSniffer, - HtmlMediaTypeSniffer, - OpdsMediaTypeSniffer, - LcpLicenseMediaTypeSniffer, - W3cWpubMediaTypeSniffer, - WebPubManifestMediaTypeSniffer, - JsonMediaTypeSniffer, - SystemMediaTypeSniffer, - ZipMediaTypeSniffer, - RarMediaTypeSniffer - ) - ) - - override fun sniffHints(hints: MediaTypeHints): Try = - sniffer.sniffHints(hints) - - override suspend fun sniffBlob(source: Readable): Try = - sniffer.sniffBlob(source) - - override suspend fun sniffContainer(container: Container): Try = - sniffer.sniffContainer(container) -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt deleted file mode 100644 index 42af6a2093..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/FormatRegistry.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.mediatype - -/** - * Registry of format metadata (e.g. file extension) associated to canonical media types. - */ -public class FormatRegistry( - fileExtensions: Map = mapOf( - MediaType.ACSM to "acsm", - MediaType.CBR to "cbr", - MediaType.CBZ to "cbz", - MediaType.DIVINA to "divina", - MediaType.DIVINA_MANIFEST to "json", - MediaType.EPUB to "epub", - MediaType.LCP_LICENSE_DOCUMENT to "lcpl", - MediaType.LCP_PROTECTED_AUDIOBOOK to "lcpa", - MediaType.LCP_PROTECTED_PDF to "lcpdf", - MediaType.PDF to "pdf", - MediaType.READIUM_AUDIOBOOK to "audiobook", - MediaType.READIUM_AUDIOBOOK_MANIFEST to "json", - MediaType.READIUM_WEBPUB to "webpub", - MediaType.READIUM_WEBPUB_MANIFEST to "json", - MediaType.W3C_WPUB_MANIFEST to "json", - MediaType.ZAB to "zab" - ), - superTypes: Map = mapOf( - MediaType.CBR to MediaType.RAR, - MediaType.CBZ to MediaType.ZIP, - MediaType.DIVINA to MediaType.READIUM_WEBPUB, - MediaType.DIVINA_MANIFEST to MediaType.READIUM_WEBPUB_MANIFEST, - MediaType.EPUB to MediaType.ZIP, - MediaType.XHTML to MediaType.XML, - MediaType.JSON_PROBLEM_DETAILS to MediaType.JSON, - MediaType.LCP_LICENSE_DOCUMENT to MediaType.JSON, - MediaType.LCP_PROTECTED_AUDIOBOOK to MediaType.READIUM_AUDIOBOOK, - MediaType.LCP_PROTECTED_PDF to MediaType.READIUM_WEBPUB, - MediaType.OPDS1 to MediaType.XML, - MediaType.OPDS1_ENTRY to MediaType.XML, - MediaType.OPDS2 to MediaType.JSON, - MediaType.OPDS2_PUBLICATION to MediaType.JSON, - MediaType.READIUM_AUDIOBOOK to MediaType.READIUM_WEBPUB, - MediaType.READIUM_AUDIOBOOK_MANIFEST to MediaType.READIUM_WEBPUB_MANIFEST, - MediaType.READIUM_WEBPUB to MediaType.ZIP, - MediaType.READIUM_WEBPUB_MANIFEST to MediaType.JSON, - MediaType.W3C_WPUB_MANIFEST to MediaType.JSON, - MediaType.ZAB to MediaType.ZIP - ) -) { - - private val fileExtensions: MutableMap = fileExtensions.toMutableMap() - - private val superTypes: MutableMap = superTypes.toMutableMap() - - /** - * Registers format data for the given [mediaType]. - */ - public fun register( - mediaType: MediaType, - fileExtension: String?, - superType: MediaType? - ) { - if (fileExtension == null) { - fileExtensions.remove(mediaType) - } else { - fileExtensions[mediaType] = fileExtension - } - - if (superType == null) { - superTypes.remove(mediaType) - } else { - superTypes[mediaType] = superType - } - } - - /** - * Returns the file extension associated to this canonical [mediaType], if any. - */ - public fun fileExtension(mediaType: MediaType): String? = - fileExtensions[mediaType] - - /** - * Returns the super type of the given [mediaType], if any. - */ - public fun superType(mediaType: MediaType): MediaType? = - superTypes[mediaType] - - /** - * Returns if [mediaType] is a generic type that could have been used instead of more specific - * media types. - */ - public fun isSuperType(mediaType: MediaType): Boolean = - superTypes.values.any { it.matches(mediaType) } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt index 72f618f91c..59e555391b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaType.kt @@ -367,7 +367,7 @@ public class MediaType private constructor( * The sniffers order is important, because some formats are subsets of other formats. */ @Deprecated(message = "Use FormatRegistry instead", level = DeprecationLevel.ERROR) - public val sniffers: MutableList = mutableListOf() + public val sniffers: MutableList = mutableListOf() /** * Resolves a format from a single file extension and media type hint, without checking the actual diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt deleted file mode 100644 index a9b9fc163f..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeRetriever.kt +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.mediatype - -import java.io.File -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.archive.ArchiveFactory -import org.readium.r2.shared.util.archive.RecursiveArchiveFactory -import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.Readable -import org.readium.r2.shared.util.file.FileResource -import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.resource.filename -import org.readium.r2.shared.util.resource.mediaType -import org.readium.r2.shared.util.tryRecover -import org.readium.r2.shared.util.use - -/** - * Retrieves a canonical [MediaType] for the provided media type and file extension hints and - * asset content. - * - * The actual format sniffing is done by the provided [mediaTypeSniffer]. - * The [DefaultMediaTypeSniffer] covers the formats supported with Readium by default. - */ -public class MediaTypeRetriever( - private val mediaTypeSniffer: MediaTypeSniffer, - private val formatRegistry: FormatRegistry, - archiveFactory: ArchiveFactory -) { - - private val archiveFactory: ArchiveFactory = - RecursiveArchiveFactory(archiveFactory, formatRegistry) - - /** - * Retrieves a canonical [MediaType] for the provided media type and file extension [hints]. - * - * Useful for testing purpose. - */ - internal fun retrieve(hints: MediaTypeHints): MediaType? = - retrieveUnsafe(hints) - .getOrNull() - - /** - * Retrieves a canonical [MediaType] for the provided [mediaType] and [fileExtension] hints. - * - * Useful for testing purpose. - */ - internal fun retrieve(mediaType: String? = null, fileExtension: String? = null): MediaType? = - retrieve( - MediaTypeHints( - mediaType = mediaType?.let { MediaType(it) }, - fileExtension = fileExtension - ) - ) - - /** - * Retrieves a canonical [MediaType] for [resource]. - * - * @param resource the resource to retrieve the media type of - * @param hints additional hints which will be added to those provided by the resource - */ - public suspend fun retrieve( - resource: Resource, - hints: MediaTypeHints = MediaTypeHints() - ): Try { - val resourceMediaType = retrieveUnsafe(resource, hints) - .getOrElse { return Try.failure(it) } - - val container = archiveFactory.create(resourceMediaType, resource) - .getOrElse { - when (it) { - is ArchiveFactory.CreateError.Reading -> - return Try.failure(MediaTypeSnifferError.Reading(it.cause)) - is ArchiveFactory.CreateError.FormatNotSupported -> - return Try.success(resourceMediaType) - } - } - - return retrieve(container, hints) - } - - /** - * Retrieves a canonical [MediaType] for [file]. - * - * @param file the file to retrieve the media type of - * @param hints additional hints which will be added to those provided by the resource - */ - public suspend fun retrieve( - file: File, - hints: MediaTypeHints = MediaTypeHints() - ): Try = - FileResource(file).use { retrieve(it, hints) } - - /** - * Retrieves a canonical [MediaType] for [container]. - * - * @param container the resource to retrieve the media type of - * @param hints media type hints - */ - public suspend fun retrieve( - container: Container, - hints: MediaTypeHints = MediaTypeHints() - ): Try { - val unsafeMediaType = retrieveUnsafe(hints) - .getOrNull() - - if (unsafeMediaType != null && !formatRegistry.isSuperType(unsafeMediaType)) { - return Try.success(unsafeMediaType) - } - - mediaTypeSniffer.sniffContainer(container) - .onSuccess { return Try.success(it) } - .onFailure { error -> - when (error) { - is MediaTypeSnifferError.NotRecognized -> {} - else -> return Try.failure(error) - } - } - - return (unsafeMediaType ?: hints.mediaTypes.firstOrNull()) - ?.let { Try.success(it) } - ?: Try.failure(MediaTypeSnifferError.NotRecognized) - } - - /** - * Retrieves a [MediaType] as much canonical as possible without accessing the content. - * - * Does not refuse too generic types. - */ - private fun retrieveUnsafe( - hints: MediaTypeHints - ): Try = - mediaTypeSniffer.sniffHints(hints) - .tryRecover { - hints.mediaTypes.firstOrNull() - ?.let { Try.success(it) } - ?: Try.failure(MediaTypeSnifferError.NotRecognized) - } - - /** - * Retrieves a [MediaType] for [resource] using [hints] added to those embedded in [resource] - * and reading content if necessary. - * - * Does not open archive resources. - */ - private suspend fun retrieveUnsafe( - resource: Resource, - hints: MediaTypeHints - ): Try { - val properties = resource.properties() - .getOrElse { return Try.failure(MediaTypeSnifferError.Reading(it)) } - - val embeddedHints = MediaTypeHints( - mediaType = properties.mediaType, - fileExtension = properties.filename - ?.substringAfterLast(".", "") - ) - - val unsafeMediaType = retrieveUnsafe(embeddedHints + hints) - .getOrNull() - - if (unsafeMediaType != null && !formatRegistry.isSuperType(unsafeMediaType)) { - return Try.success(unsafeMediaType) - } - - mediaTypeSniffer.sniffBlob(resource) - .onSuccess { return Try.success(it) } - .onFailure { error -> - when (error) { - is MediaTypeSnifferError.NotRecognized -> {} - else -> return Try.failure(error) - } - } - - return (unsafeMediaType ?: hints.mediaTypes.firstOrNull()) - ?.let { Try.success(it) } - ?: Try.failure(MediaTypeSnifferError.NotRecognized) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt deleted file mode 100644 index 783a775e2a..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/mediatype/MediaTypeSniffer.kt +++ /dev/null @@ -1,894 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.mediatype - -import android.webkit.MimeTypeMap -import java.io.IOException -import java.net.URLConnection -import java.util.Locale -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.json.JSONObject -import org.readium.r2.shared.extensions.findInstance -import org.readium.r2.shared.extensions.tryOrNull -import org.readium.r2.shared.publication.Link -import org.readium.r2.shared.publication.Manifest -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.RelativeUrl -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.Readable -import org.readium.r2.shared.util.data.asInputStream -import org.readium.r2.shared.util.data.borrow -import org.readium.r2.shared.util.data.decodeJson -import org.readium.r2.shared.util.data.decodeRwpm -import org.readium.r2.shared.util.data.decodeString -import org.readium.r2.shared.util.data.decodeXml -import org.readium.r2.shared.util.data.readDecodeOrElse -import org.readium.r2.shared.util.getOrDefault -import org.readium.r2.shared.util.getOrElse - -public sealed class MediaTypeSnifferError( - override val message: String, - override val cause: Error? -) : Error { - public data object NotRecognized : - MediaTypeSnifferError("Media type of resource could not be inferred.", null) - - public data class Reading(override val cause: ReadError) : - MediaTypeSnifferError("An error occurred while trying to read content.", cause) -} - -/** - * Sniffs a [MediaType] from media type and file extension hints. - */ -public interface HintMediaTypeSniffer { - - public fun sniffHints( - hints: MediaTypeHints - ): Try -} - -/** - * Sniffs a [MediaType] from a [Readable] blob. - */ -public interface BlobMediaTypeSniffer { - - public suspend fun sniffBlob( - source: Readable - ): Try -} - -/** - * Sniffs a [MediaType] from a [Container]. - */ -public interface ContainerMediaTypeSniffer { - - public suspend fun sniffContainer( - container: Container - ): Try -} - -/** - * Sniffs a [MediaType] from media type and file extension hints or asset content. - */ -public interface MediaTypeSniffer : - HintMediaTypeSniffer, - BlobMediaTypeSniffer, - ContainerMediaTypeSniffer { - - public override fun sniffHints( - hints: MediaTypeHints - ): Try = - Try.failure(MediaTypeSnifferError.NotRecognized) - - public override suspend fun sniffBlob( - source: Readable - ): Try = - Try.failure(MediaTypeSnifferError.NotRecognized) - - public override suspend fun sniffContainer( - container: Container - ): Try = - Try.failure(MediaTypeSnifferError.NotRecognized) -} - -public class CompositeMediaTypeSniffer( - private val sniffers: List -) : MediaTypeSniffer { - - public constructor(vararg sniffers: MediaTypeSniffer) : this(sniffers.toList()) - - override fun sniffHints(hints: MediaTypeHints): Try { - for (sniffer in sniffers) { - sniffer.sniffHints(hints) - .getOrNull() - ?.let { return Try.success(it) } - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffBlob(source: Readable): Try { - for (sniffer in sniffers) { - sniffer.sniffBlob(source) - .getOrElse { error -> - when (error) { - MediaTypeSnifferError.NotRecognized -> - null - else -> - return Try.failure(error) - } - } - ?.let { return Try.success(it) } - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffContainer(container: Container): Try { - for (sniffer in sniffers) { - sniffer.sniffContainer(container) - .getOrElse { error -> - when (error) { - MediaTypeSnifferError.NotRecognized -> - null - else -> - return Try.failure(error) - } - } - ?.let { return Try.success(it) } - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** - * Sniffs an XHTML document. - * - * Must precede the HTML sniffer. - */ -public object XhtmlMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { - if ( - hints.hasFileExtension("xht", "xhtml") || - hints.hasMediaType("application/xhtml+xml") - ) { - return Try.success(MediaType.XHTML) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffBlob(source: Readable): Try { - if (!source.canReadWholeBlob()) { - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - source.readDecodeOrElse( - decode = { it.decodeXml() }, - recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, - recoverDecode = { null } - )?.takeIf { - it.name.lowercase(Locale.ROOT) == "html" && - it.namespace.lowercase(Locale.ROOT).contains("xhtml") - }?.let { - return Try.success(MediaType.XHTML) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** Sniffs an HTML document. */ -public object HtmlMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { - if ( - hints.hasFileExtension("htm", "html") || - hints.hasMediaType("text/html") - ) { - return Try.success(MediaType.HTML) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffBlob(source: Readable): Try { - if (!source.canReadWholeBlob()) { - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - // [contentAsXml] will fail if the HTML is not a proper XML document, hence the doctype check. - source.readDecodeOrElse( - decode = { it.decodeXml() }, - recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, - recoverDecode = { null } - ) - ?.takeIf { it.name.lowercase(Locale.ROOT) == "html" } - ?.let { return Try.success(MediaType.HTML) } - - source.readDecodeOrElse( - decode = { it.decodeString() }, - recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, - recoverDecode = { null } - ) - ?.takeIf { it.trimStart().take(15).lowercase() == "" } - ?.let { return Try.success(MediaType.HTML) } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** Sniffs an OPDS document. */ -public object OpdsMediaTypeSniffer : MediaTypeSniffer { - - override fun sniffHints(hints: MediaTypeHints): Try { - // OPDS 1 - if (hints.hasMediaType("application/atom+xml;type=entry;profile=opds-catalog")) { - return Try.success(MediaType.OPDS1_ENTRY) - } - if (hints.hasMediaType("application/atom+xml;profile=opds-catalog;kind=navigation")) { - return Try.success(MediaType.OPDS1_NAVIGATION_FEED) - } - if (hints.hasMediaType("application/atom+xml;profile=opds-catalog;kind=acquisition")) { - return Try.success(MediaType.OPDS1_ACQUISITION_FEED) - } - if (hints.hasMediaType("application/atom+xml;profile=opds-catalog")) { - return Try.success(MediaType.OPDS1) - } - - // OPDS 2 - if (hints.hasMediaType("application/opds+json")) { - return Try.success(MediaType.OPDS2) - } - if (hints.hasMediaType("application/opds-publication+json")) { - return Try.success(MediaType.OPDS2_PUBLICATION) - } - - // OPDS Authentication Document. - if ( - hints.hasMediaType("application/opds-authentication+json") || - hints.hasMediaType("application/vnd.opds.authentication.v1.0+json") - ) { - return Try.success(MediaType.OPDS_AUTHENTICATION) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffBlob(source: Readable): Try { - if (!source.canReadWholeBlob()) { - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - // OPDS 1 - source.readDecodeOrElse( - decode = { it.decodeXml() }, - recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, - recoverDecode = { null } - )?.takeIf { it.namespace == "http://www.w3.org/2005/Atom" } - ?.let { xml -> - if (xml.name == "feed") { - return Try.success(MediaType.OPDS1) - } else if (xml.name == "entry") { - return Try.success(MediaType.OPDS1_ENTRY) - } - } - - // OPDS 2 - source.readDecodeOrElse( - decode = { it.decodeRwpm() }, - recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, - recoverDecode = { null } - ) - ?.let { rwpm -> - if (rwpm.linkWithRel("self")?.mediaType?.matches("application/opds+json") == true - ) { - return Try.success(MediaType.OPDS2) - } - - /** - * Finds the first [Link] having a relation matching the given [predicate]. - */ - fun List.firstWithRelMatching(predicate: (String) -> Boolean): Link? = - firstOrNull { it.rels.any(predicate) } - - if (rwpm.links.firstWithRelMatching { - it.startsWith( - "http://opds-spec.org/acquisition" - ) - } != null - ) { - return Try.success(MediaType.OPDS2_PUBLICATION) - } - } - - // OPDS Authentication Document. - source.containsJsonKeys("id", "title", "authentication") - .getOrElse { return Try.failure(MediaTypeSnifferError.Reading(it)) } - .takeIf { it } - ?.let { return Try.success(MediaType.OPDS_AUTHENTICATION) } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** Sniffs an LCP License Document. */ -public object LcpLicenseMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { - if ( - hints.hasFileExtension("lcpl") || - hints.hasMediaType("application/vnd.readium.lcp.license.v1.0+json") - ) { - return Try.success(MediaType.LCP_LICENSE_DOCUMENT) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffBlob(source: Readable): Try { - if (!source.canReadWholeBlob()) { - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - source.containsJsonKeys("id", "issued", "provider", "encryption") - .getOrElse { return Try.failure(MediaTypeSnifferError.Reading(it)) } - .takeIf { it } - ?.let { return Try.success(MediaType.LCP_LICENSE_DOCUMENT) } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** Sniffs a bitmap image. */ -public object BitmapMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { - if ( - hints.hasFileExtension("avif") || - hints.hasMediaType("image/avif") - ) { - return Try.success(MediaType.AVIF) - } - if ( - hints.hasFileExtension("bmp", "dib") || - hints.hasMediaType("image/bmp", "image/x-bmp") - ) { - return Try.success(MediaType.BMP) - } - if ( - hints.hasFileExtension("gif") || - hints.hasMediaType("image/gif") - ) { - return Try.success(MediaType.GIF) - } - if ( - hints.hasFileExtension("jpg", "jpeg", "jpe", "jif", "jfif", "jfi") || - hints.hasMediaType("image/jpeg") - ) { - return Try.success(MediaType.JPEG) - } - if ( - hints.hasFileExtension("jxl") || - hints.hasMediaType("image/jxl") - ) { - return Try.success(MediaType.JXL) - } - if ( - hints.hasFileExtension("png") || - hints.hasMediaType("image/png") - ) { - return Try.success(MediaType.PNG) - } - if ( - hints.hasFileExtension("tiff", "tif") || - hints.hasMediaType("image/tiff", "image/tiff-fx") - ) { - return Try.success(MediaType.TIFF) - } - if ( - hints.hasFileExtension("webp") || - hints.hasMediaType("image/webp") - ) { - return Try.success(MediaType.WEBP) - } - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** Sniffs a Readium Web Manifest. */ -public object WebPubManifestMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { - if (hints.hasMediaType("application/audiobook+json")) { - return Try.success(MediaType.READIUM_AUDIOBOOK_MANIFEST) - } - - if (hints.hasMediaType("application/divina+json")) { - return Try.success(MediaType.DIVINA_MANIFEST) - } - - if (hints.hasMediaType("application/webpub+json")) { - return Try.success(MediaType.READIUM_WEBPUB_MANIFEST) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - public override suspend fun sniffBlob(source: Readable): Try { - if (!source.canReadWholeBlob()) { - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - val manifest: Manifest = - source.readDecodeOrElse( - decode = { it.decodeRwpm() }, - recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, - recoverDecode = { null } - ) ?: return Try.failure(MediaTypeSnifferError.NotRecognized) - - if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { - return Try.success(MediaType.READIUM_AUDIOBOOK_MANIFEST) - } - - if (manifest.conformsTo(Publication.Profile.DIVINA)) { - return Try.success(MediaType.DIVINA_MANIFEST) - } - if (manifest.linkWithRel("self")?.mediaType?.matches("application/webpub+json") == true) { - return Try.success(MediaType.READIUM_WEBPUB_MANIFEST) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** Sniffs a Readium Web Publication, protected or not by LCP. */ -public object WebPubMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { - if ( - hints.hasFileExtension("audiobook") || - hints.hasMediaType("application/audiobook+zip") - ) { - return Try.success(MediaType.READIUM_AUDIOBOOK) - } - - if ( - hints.hasFileExtension("divina") || - hints.hasMediaType("application/divina+zip") - ) { - return Try.success(MediaType.DIVINA) - } - - if ( - hints.hasFileExtension("webpub") || - hints.hasMediaType("application/webpub+zip") - ) { - return Try.success(MediaType.READIUM_WEBPUB) - } - - if ( - hints.hasFileExtension("lcpa") || - hints.hasMediaType("application/audiobook+lcp") - ) { - return Try.success(MediaType.LCP_PROTECTED_AUDIOBOOK) - } - if ( - hints.hasFileExtension("lcpdf") || - hints.hasMediaType("application/pdf+lcp") - ) { - return Try.success(MediaType.LCP_PROTECTED_PDF) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffContainer(container: Container): Try { - // Reads a RWPM from a manifest.json archive entry. - val manifest: Manifest = - container[RelativeUrl("manifest.json")!!] - ?.read() - ?.getOrElse { error -> - return Try.failure(MediaTypeSnifferError.Reading(error)) - } - ?.let { tryOrNull { Manifest.fromJSON(JSONObject(String(it))) } } - ?: return Try.failure(MediaTypeSnifferError.NotRecognized) - - val isLcpProtected = RelativeUrl("license.lcpl")!! in container - - if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { - return if (isLcpProtected) { - Try.success(MediaType.LCP_PROTECTED_AUDIOBOOK) - } else { - Try.success(MediaType.READIUM_AUDIOBOOK) - } - } - if (manifest.conformsTo(Publication.Profile.DIVINA)) { - return Try.success(MediaType.DIVINA) - } - if (isLcpProtected && manifest.conformsTo(Publication.Profile.PDF)) { - return Try.success(MediaType.LCP_PROTECTED_PDF) - } - if (manifest.linkWithRel("self")?.mediaType?.matches("application/webpub+json") == true) { - return Try.success(MediaType.READIUM_WEBPUB) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** Sniffs a W3C Web Publication Manifest. */ -public object W3cWpubMediaTypeSniffer : MediaTypeSniffer { - override suspend fun sniffBlob(source: Readable): Try { - if (!source.canReadWholeBlob()) { - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. - val string = source.readDecodeOrElse( - decode = { it.decodeString() }, - recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, - recoverDecode = { "" } - ) - if ( - string.contains("@context") && - string.contains("https://www.w3.org/ns/wp-context") - ) { - return Try.success(MediaType.W3C_WPUB_MANIFEST) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** - * Sniffs an EPUB publication. - * - * Reference: https://www.w3.org/publishing/epub3/epub-ocf.html#sec-zip-container-mime - */ -public object EpubMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { - if ( - hints.hasFileExtension("epub") || - hints.hasMediaType("application/epub+zip") - ) { - return Try.success(MediaType.EPUB) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffContainer(container: Container): Try { - val mimetype = container[RelativeUrl("mimetype")!!] - ?.readDecodeOrElse( - decode = { it.decodeString() }, - recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, - recoverDecode = { null } - )?.trim() - - if (mimetype == "application/epub+zip") { - return Try.success(MediaType.EPUB) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** - * Sniffs a Lightweight Packaging Format (LPF). - * - * References: - * - https://www.w3.org/TR/lpf/ - * - https://www.w3.org/TR/pub-manifest/ - */ -public object LpfMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { - if ( - hints.hasFileExtension("lpf") || - hints.hasMediaType("application/lpf+zip") - ) { - return Try.success(MediaType.LPF) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffContainer(container: Container): Try { - if (RelativeUrl("index.html")!! in container) { - return Try.success(MediaType.LPF) - } - - // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. - container[RelativeUrl("publication.json")!!] - ?.read() - ?.getOrElse { error -> - return Try.failure(MediaTypeSnifferError.Reading(error)) - } - ?.let { tryOrNull { String(it) } } - ?.let { manifest -> - if ( - manifest.contains("@context") && - manifest.contains("https://www.w3.org/ns/pub-context") - ) { - return Try.success(MediaType.LPF) - } - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** - * Sniffs a RAR archive. - * - * At the moment, only hints are supported. - */ -public object RarMediaTypeSniffer : MediaTypeSniffer { - - override fun sniffHints(hints: MediaTypeHints): Try { - if ( - hints.hasFileExtension("rar") || - hints.hasMediaType("application/vnd.rar") || - hints.hasMediaType("application/x-rar") || - hints.hasMediaType("application/x-rar-compressed") - ) { - return Try.success(MediaType.RAR) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** - * Sniffs a simple Archive-based publication format, like Comic Book Archive or Zipped Audio Book. - * - * Reference: https://wiki.mobileread.com/wiki/CBR_and_CBZ - */ -public object ArchiveMediaTypeSniffer : MediaTypeSniffer { - - /** - * Authorized extensions for resources in a CBZ archive. - * Reference: https://wiki.mobileread.com/wiki/CBR_and_CBZ - */ - private val cbzExtensions = listOf( - // bitmap - "bmp", "dib", "gif", "jif", "jfi", "jfif", "jpg", "jpeg", "png", "tif", "tiff", "webp", - // metadata - "acbf", "xml" - ) - - /** - * Authorized extensions for resources in a ZAB archive (Zipped Audio Book). - */ - private val zabExtensions = listOf( - // audio - "aac", - "aiff", - "alac", - "flac", - "m4a", - "m4b", - "mp3", - "ogg", - "oga", - "mogg", - "opus", - "wav", - "webm", - // playlist - "asx", - "bio", - "m3u", - "m3u8", - "pla", - "pls", - "smil", - "vlc", - "wpl", - "xspf", - "zpl" - ) - - override fun sniffHints(hints: MediaTypeHints): Try { - if ( - hints.hasFileExtension("cbz") || - hints.hasMediaType( - "application/vnd.comicbook+zip", - "application/x-cbz" - ) - ) { - return Try.success(MediaType.CBZ) - } - - if ( - hints.hasFileExtension("cbr") || - hints.hasMediaType("application/vnd.comicbook-rar") || - hints.hasMediaType("application/x-cbr") - ) { - return Try.success(MediaType.CBR) - } - - if (hints.hasFileExtension("zab")) { - return Try.success(MediaType.ZAB) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffContainer(container: Container): Try { - fun isIgnored(url: Url): Boolean = - url.filename?.startsWith(".") == true || url.filename == "Thumbs.db" - - fun archiveContainsOnlyExtensions(fileExtensions: List): Boolean = - container.all { url -> - isIgnored(url) || url.extension?.let { - fileExtensions.contains( - it.lowercase(Locale.ROOT) - ) - } == true - } - - if ( - archiveContainsOnlyExtensions(cbzExtensions) && - container.archiveMediaType?.matches(MediaType.ZIP) == true - ) { - return Try.success(MediaType.CBZ) - } - - if ( - archiveContainsOnlyExtensions(cbzExtensions) && - container.archiveMediaType?.matches(MediaType.RAR) == true - ) { - return Try.success(MediaType.CBR) - } - - if ( - archiveContainsOnlyExtensions(zabExtensions) && - container.archiveMediaType?.matches(MediaType.ZIP) == true - ) { - return Try.success(MediaType.ZAB) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** - * Sniffs a PDF document. - * - * Reference: https://www.loc.gov/preservation/digital/formats/fdd/fdd000123.shtml - */ -public object PdfMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { - if ( - hints.hasFileExtension("pdf") || - hints.hasMediaType("application/pdf") - ) { - return Try.success(MediaType.PDF) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffBlob(source: Readable): Try { - source.read(0L until 5L) - .getOrElse { error -> - return Try.failure(MediaTypeSnifferError.Reading(error)) - } - .let { tryOrNull { it.toString(Charsets.UTF_8) } } - .takeIf { it == "%PDF-" } - ?.let { return Try.success(MediaType.PDF) } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** Sniffs a JSON document. */ -public object JsonMediaTypeSniffer : MediaTypeSniffer { - override fun sniffHints(hints: MediaTypeHints): Try { - if (hints.hasMediaType("application/problem+json")) { - return Try.success(MediaType.JSON_PROBLEM_DETAILS) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffBlob(source: Readable): Try { - if (!source.canReadWholeBlob()) { - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - source.readDecodeOrElse( - decode = { it.decodeJson() }, - recoverRead = { return Try.failure(MediaTypeSnifferError.Reading(it)) }, - recoverDecode = { null } - )?.let { return Try.success(MediaType.JSON) } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } -} - -/** - * Sniffs the system-wide registered media types using [MimeTypeMap] and - * [URLConnection.guessContentTypeFromStream]. - */ -public object SystemMediaTypeSniffer : MediaTypeSniffer { - - private val mimetypes = tryOrNull { MimeTypeMap.getSingleton() } - - override fun sniffHints(hints: MediaTypeHints): Try { - for (mediaType in hints.mediaTypes) { - sniffType(mediaType.toString()) - ?.let { return Try.success(it) } - } - - for (extension in hints.fileExtensions) { - sniffExtension(extension) - ?.let { return Try.success(it) } - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffBlob(source: Readable): Try { - source.borrow() - .asInputStream(wrapError = ::SystemSnifferException) - .use { stream -> - try { - withContext(Dispatchers.IO) { - URLConnection.guessContentTypeFromStream(stream) - ?.let { sniffType(it) } - } - } catch (e: Exception) { - e.findInstance(SystemSnifferException::class.java) - ?.let { - return Try.failure( - MediaTypeSnifferError.Reading(it.error) - ) - } - } - } - ?.let { return Try.success(it) } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - private class SystemSnifferException( - val error: ReadError - ) : IOException() - - private fun sniffType(type: String): MediaType? { - val extension = mimetypes?.getExtensionFromMimeType(type) - ?: return null - val preferredType = mimetypes.getMimeTypeFromExtension(extension) - ?: return null - return MediaType(preferredType) - } - - private fun sniffExtension(extension: String): MediaType? = - mimetypes?.getMimeTypeFromExtension(extension) - ?.let { MediaType(it) } -} - -private suspend fun Readable.canReadWholeBlob() = - length().getOrDefault(0) < 5 * 1000 * 1000 - -/** - * Returns whether the content is a JSON object containing all of the given root keys. - */ -@Suppress("SameParameterValue") -private suspend fun Readable.containsJsonKeys( - vararg keys: String -): Try { - val json = readDecodeOrElse( - decode = { it.decodeJson() }, - recoverRead = { return Try.failure(it) }, - recoverDecode = { return Try.success(false) } - ) - return Try.success(json.keys().asSequence().toSet().containsAll(keys.toList())) -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProperties.kt similarity index 95% rename from readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProperties.kt index a1920221bf..677c2c1129 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveProperties.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ArchiveProperties.kt @@ -4,14 +4,13 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.archive +package org.readium.r2.shared.util.resource import org.json.JSONObject import org.readium.r2.shared.JSONable import org.readium.r2.shared.extensions.optNullableBoolean import org.readium.r2.shared.extensions.optNullableLong import org.readium.r2.shared.extensions.toMap -import org.readium.r2.shared.util.resource.Resource /** * Holds information about how the resource is stored in the archive. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt index 8d90a79c48..d981c68beb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt @@ -6,12 +6,9 @@ package org.readium.r2.shared.util.resource -import kotlin.String -import kotlin.let 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.mediatype.MediaType /** * A factory to read [Resource]s from [Url]s. @@ -33,11 +30,9 @@ public interface ResourceFactory { * Creates a [Resource] to access [url]. * * @param url The url the resource will access. - * @param mediaType media type of the resource if known. */ public suspend fun create( - url: AbsoluteUrl, - mediaType: MediaType? = null + url: AbsoluteUrl ): Try } @@ -53,10 +48,9 @@ public class CompositeResourceFactory( override suspend fun create( url: AbsoluteUrl, - mediaType: MediaType? ): Try { for (factory in factories) { - factory.create(url, mediaType) + factory.create(url) .getOrNull() ?.let { return Try.success(it) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/ContentSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/ContentSniffer.kt new file mode 100644 index 0000000000..0e7d8c7b38 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/ContentSniffer.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.sniff + +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.getOrElse +import timber.log.Timber + +/** + * Tries to refine a [Format] from media type and file extension hints. + */ +public interface FormatHintsSniffer { + + public fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? +} + +/** + * Tries to refine a [Format] by sniffing a [Readable] blob. + */ +public interface BlobSniffer { + + public suspend fun sniffBlob( + format: Format?, + source: Readable + ): Try +} + +/** + * Tries to Refine a [Format] by sniffing a [Container]. + */ +public interface ContainerSniffer { + + public suspend fun sniffContainer( + format: Format?, + container: Container + ): Try +} + +public interface ContentSniffer : + FormatHintsSniffer, + BlobSniffer, + ContainerSniffer { + + public override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? = + null + + public override suspend fun sniffBlob( + format: Format?, + source: Readable + ): Try = + Try.success(format) + + + public override suspend fun sniffContainer( + format: Format?, + container: Container + ): Try = + Try.success(format) +} + +public class CompositeContentSniffer( + private val sniffers: List +) : ContentSniffer { + + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + for (sniffer in sniffers) { + Timber.d("Trying hints ${sniffer.javaClass.simpleName}") + sniffer.sniffHints(format, hints) + .takeIf { it != format } + ?.let { return it } + } + + return format + } + + override suspend fun sniffBlob( + format: Format?, + source: Readable + ): Try { + for (sniffer in sniffers) { + Timber.d("Trying blob ${sniffer.javaClass.simpleName}") + sniffer.sniffBlob(format, source) + .getOrElse { return Try.failure(it) } + .takeIf { it != format } + ?.let { return Try.success(it) } + } + + return Try.success(format) + } + + override suspend fun sniffContainer( + format: Format?, + container: Container + ): Try { + for (sniffer in sniffers) { + Timber.d("Trying container ${sniffer.javaClass.simpleName}") + sniffer.sniffContainer(format, container) + .getOrElse { return Try.failure(it) } + .takeIf { it != format } + ?.let { return Try.success(it) } + } + + return Try.success(format) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/DefaultSniffers.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/DefaultSniffers.kt new file mode 100644 index 0000000000..054cbb72d3 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/DefaultSniffers.kt @@ -0,0 +1,983 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.sniff + +import java.util.Locale +import org.json.JSONObject +import org.readium.r2.shared.extensions.tryOrNull +import org.readium.r2.shared.publication.Link +import org.readium.r2.shared.publication.Manifest +import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.publication.encryption.encryption +import org.readium.r2.shared.publication.protection.EpubEncryption +import org.readium.r2.shared.util.RelativeUrl +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.data.decodeJson +import org.readium.r2.shared.util.data.decodeRwpm +import org.readium.r2.shared.util.data.decodeString +import org.readium.r2.shared.util.data.decodeXml +import org.readium.r2.shared.util.data.readDecodeOrElse +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.getOrDefault +import org.readium.r2.shared.util.getOrElse + +public object DefaultContentSniffer + : ContentSniffer by CompositeContentSniffer( + listOf( + RpfSniffer, + EpubSniffer, + LpfSniffer, + ArchiveSniffer, + PdfSniffer, + BitmapSniffer, + XhtmlSniffer, + HtmlSniffer, + OpdsSniffer, + LcpLicenseSniffer, + LcpSniffer, + W3cWpubSniffer, + RwpmSniffer, + JsonSniffer, + ZipSniffer, + RarSniffer + ) +) + +/** + * Sniffs an XHTML document. + * + * Must precede the HTML sniffer. + */ +public object XhtmlSniffer : ContentSniffer { + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + if ( + hints.hasFileExtension("xht", "xhtml") || + hints.hasMediaType("application/xhtml+xml") + ) { + return Format.XHTML + } + + return format + } + + override suspend fun sniffBlob( + format: Format?, + source: Readable + ): Try { + if (format?.conformsTo(Format.XML) == false || !source.canReadWholeBlob()) { + return Try.success(format) + } + + source.readDecodeOrElse( + decode = { it.decodeXml() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + )?.takeIf { + it.name.lowercase(Locale.ROOT) == "html" && + it.namespace.lowercase(Locale.ROOT).contains("xhtml") + }?.let { + return Try.success(Format.XHTML) + } + + return Try.success(format) + } +} + +/** Sniffs an HTML document. */ +public object HtmlSniffer : ContentSniffer { + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + if ( + hints.hasFileExtension("htm", "html") || + hints.hasMediaType("text/html") + ) { + return Format.HTML + } + + return format + } + + override suspend fun sniffBlob( + format: Format?, + source: Readable + ): Try { + if (!source.canReadWholeBlob()) { + return Try.success(format) + } + + // decodeXml will fail if the HTML is not a proper XML document, hence the doctype check. + source.readDecodeOrElse( + decode = { it.decodeXml() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + ) + ?.takeIf { it.name.lowercase(Locale.ROOT) == "html" } + ?.let { return Try.success(Format.HTML) } + + source.readDecodeOrElse( + decode = { it.decodeString() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + ) + ?.takeIf { it.trimStart().take(15).lowercase() == "" } + ?.let { return Try.success(Format.HTML) } + + return Try.success(format) + } +} + +/** Sniffs an OPDS1 document. */ +public object OpdsSniffer : ContentSniffer { + + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + return sniffHintsXml(format, hints) + ?: sniffHintsJson(format, hints) + } + + private fun sniffHintsXml(format: Format?, hints: FormatHints): Format? { + // OPDS 1 + if (hints.hasMediaType("application/atom+xml;type=entry;profile=opds-catalog")) { + return Format.OPDS1_ENTRY + } + if (hints.hasMediaType("application/atom+xml;profile=opds-catalog;kind=navigation")) { + return Format.OPDS1_NAVIGATION_FEED + } + if (hints.hasMediaType("application/atom+xml;profile=opds-catalog;kind=acquisition")) { + return Format.OPDS1_ACQUISITION_FEED + } + if (hints.hasMediaType("application/atom+xml;profile=opds-catalog")) { + return Format.OPDS1 + } + + return format + } + + private fun sniffHintsJson(format: Format?, hints: FormatHints): Format? { + // OPDS 2 + if (hints.hasMediaType("application/opds+json")) { + return Format.OPDS2 + } + if (hints.hasMediaType("application/opds-publication+json")) { + return Format.OPDS2_PUBLICATION + } + + // OPDS Authentication Document. + if ( + hints.hasMediaType("application/opds-authentication+json") || + hints.hasMediaType("application/vnd.opds.authentication.v1.0+json") + ) { + return Format.OPDS_AUTHENTICATION + } + + return format + } + + override suspend fun sniffBlob( + format: Format?, + source: Readable + ): Try { + if (!source.canReadWholeBlob() ) { + return Try.success(format) + } + + sniffBlobXml(format, source) + .getOrElse { return Try.failure(it) } + ?.let { return Try.success(it) } + + sniffBlobJson(format, source) + .getOrElse { return Try.failure(it) } + ?.let { return Try.success(it) } + + return Try.success(format) + } + + private suspend fun sniffBlobXml(format: Format?, source: Readable): Try { + if (format?.conformsTo(Format.XML) == false) { + return Try.success(format) + } + + // OPDS 1 + source.readDecodeOrElse( + decode = { it.decodeXml() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + )?.takeIf { it.namespace == "http://www.w3.org/2005/Atom" } + ?.let { xml -> + if (xml.name == "feed") { + return Try.success(Format.OPDS1) + } else if (xml.name == "entry") { + return Try.success(Format.OPDS1_ENTRY) + } + } + + return Try.success(format) + } + + private suspend fun sniffBlobJson(format: Format?, source: Readable): Try { + if (format?.conformsTo(Format.JSON) == false) { + return Try.success(format) + } + + // OPDS 2 + source.readDecodeOrElse( + decode = { it.decodeRwpm() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + ) + ?.let { rwpm -> + if (rwpm.linkWithRel("self")?.mediaType?.matches("application/opds+json") == true + ) { + return Try.success(Format.OPDS2) + } + + /** + * Finds the first [Link] having a relation matching the given [predicate]. + */ + fun List.firstWithRelMatching(predicate: (String) -> Boolean): Link? = + firstOrNull { it.rels.any(predicate) } + + if (rwpm.links.firstWithRelMatching { + it.startsWith( + "http://opds-spec.org/acquisition" + ) + } != null + ) { + return Try.success(Format.OPDS2_PUBLICATION) + } + } + + // OPDS Authentication Document. + source.containsJsonKeys("id", "title", "authentication") + .getOrElse { return Try.failure(it) } + .takeIf { it } + ?.let { return Try.success(Format.OPDS_AUTHENTICATION) } + + return Try.success(format) + } +} + +/** Sniffs an LCP License Document. */ +public object LcpLicenseSniffer : ContentSniffer { + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + if ( + hints.hasFileExtension("lcpl") || + hints.hasMediaType("application/vnd.readium.lcp.license.v1.0+json") + ) { + return Format.LCP_LICENSE_DOCUMENT + } + + return format + } + + override suspend fun sniffBlob( + format: Format?, + source: Readable + ): Try { + if ( + format?.conformsTo(Format.JSON) == false || + !source.canReadWholeBlob() + ) { + return Try.success(format) + } + + source.containsJsonKeys("id", "issued", "provider", "encryption") + .getOrElse { return Try.failure(it) } + .takeIf { it } + ?.let { return Try.success(Format.LCP_LICENSE_DOCUMENT) } + + return Try.success(format) + } +} + +/** Sniffs a bitmap image. */ +public object BitmapSniffer : ContentSniffer { + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + if ( + hints.hasFileExtension("avif") || + hints.hasMediaType("image/avif") + ) { + return Format.AVIF + } + if ( + hints.hasFileExtension("bmp", "dib") || + hints.hasMediaType("image/bmp", "image/x-bmp") + ) { + return Format.BMP + } + if ( + hints.hasFileExtension("gif") || + hints.hasMediaType("image/gif") + ) { + return Format.GIF + } + if ( + hints.hasFileExtension("jpg", "jpeg", "jpe", "jif", "jfif", "jfi") || + hints.hasMediaType("image/jpeg") + ) { + return Format.JPEG + } + if ( + hints.hasFileExtension("jxl") || + hints.hasMediaType("image/jxl") + ) { + return Format.JXL + } + if ( + hints.hasFileExtension("png") || + hints.hasMediaType("image/png") + ) { + return Format.PNG + } + if ( + hints.hasFileExtension("tiff", "tif") || + hints.hasMediaType("image/tiff", "image/tiff-fx") + ) { + return Format.TIFF + } + if ( + hints.hasFileExtension("webp") || + hints.hasMediaType("image/webp") + ) { + return Format.WEBP + } + return format + } +} + +/** Sniffs a Readium Web Manifest. */ +public object RwpmSniffer : ContentSniffer { + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + if (hints.hasMediaType("application/audiobook+json")) { + return Format.RWPM_AUDIO + } + + if (hints.hasMediaType("application/divina+json")) { + return Format.RWPM_IMAGE + } + + if (hints.hasMediaType("application/webpub+json")) { + return Format.RWPM + } + + return format + } + + public override suspend fun sniffBlob( + format: Format?, + source: Readable + ): Try { + if ( + format?.conformsTo(Format.JSON) == false || + !source.canReadWholeBlob() + ) { + return Try.success(format) + } + + val manifest: Manifest = + source.readDecodeOrElse( + decode = { it.decodeRwpm() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + ) ?: return Try.success(format) + + if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { + return Try.success(Format.RWPM_AUDIO) + } + + if (manifest.conformsTo(Publication.Profile.DIVINA)) { + return Try.success(Format.RWPM_IMAGE) + } + if (manifest.linkWithRel("self")?.mediaType?.matches("application/webpub+json") == true) { + return Try.success(Format.RWPM) + } + + return Try.success(format) + } +} + +/** Sniffs a Readium Web Publication, protected or not by LCP. */ +public object RpfSniffer : ContentSniffer { + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + if ( + hints.hasFileExtension("audiobook") || + hints.hasMediaType("application/audiobook+zip") + ) { + return Format.RPF_AUDIO + } + + if ( + hints.hasFileExtension("divina") || + hints.hasMediaType("application/divina+zip") + ) { + return Format.RPF_IMAGE + } + + if ( + hints.hasFileExtension("webpub") || + hints.hasMediaType("application/webpub+zip") + ) { + return Format.RPF + } + + if ( + hints.hasFileExtension("lcpa") || + hints.hasMediaType("application/audiobook+lcp") + ) { + return Format.RPF_AUDIO_LCP + } + if ( + hints.hasFileExtension("lcpdf") || + hints.hasMediaType("application/pdf+lcp") + ) { + return Format.RPF_PDF_LCP + } + + return format + } + + override suspend fun sniffContainer( + format: Format?, + container: Container + ): Try { + // Recognize exploded RPF. + if (format?.conformsTo(Format.ZIP) == false) { + return Try.success(format) + } + + // Reads a RWPM from a manifest.json archive entry. + val manifest: Manifest = + container[RelativeUrl("manifest.json")!!] + ?.read() + ?.getOrElse { return Try.failure(it) } + ?.let { tryOrNull { Manifest.fromJSON(JSONObject(String(it))) } } + ?: return Try.success(format) + + if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { + return Try.success(Format.RPF_AUDIO) + } + if (manifest.conformsTo(Publication.Profile.DIVINA)) { + return Try.success(Format.RPF_IMAGE) + } + if (manifest.conformsTo(Publication.Profile.PDF)) { + return Try.success(Format.RPF_PDF) + } + if (manifest.linkWithRel("self")?.mediaType?.matches("application/webpub+json") == true) { + Try.success(Format.RPF) + } + + return Try.success(format) + } +} + +/** Sniffs a W3C Web Publication Manifest. */ +public object W3cWpubSniffer : ContentSniffer { + + override suspend fun sniffBlob( + format: Format?, + source: Readable + ): Try { + if (!source.canReadWholeBlob() || format?.conformsTo(Format.JSON) == false) { + return Try.success(format) + } + + // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. + val string = source.readDecodeOrElse( + decode = { it.decodeString() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { "" } + ) + if ( + string.contains("@context") && + string.contains("https://www.w3.org/ns/wp-context") + ) { + return Try.success(Format.W3C_WPUB_MANIFEST) + } + + return Try.success(format) + } +} + +/** + * Sniffs an EPUB publication. + * + * Reference: https://www.w3.org/publishing/epub3/epub-ocf.html#sec-zip-container-mime + */ +public object EpubSniffer : ContentSniffer { + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + if ( + hints.hasFileExtension("epub") || + hints.hasMediaType("application/epub+zip") + ) { + return Format.EPUB + } + + return format + } + + override suspend fun sniffContainer( + format: Format?, + container: Container + ): Try { + // Recognize exploded EPUBs. + if (format?.conformsTo(Format.ZIP) == false) { + return Try.success(format) + } + + val mimetype = container[RelativeUrl("mimetype")!!] + ?.readDecodeOrElse( + decode = { it.decodeString() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + )?.trim() + + if (mimetype == "application/epub+zip") { + return Try.success(Format.EPUB) + } + + return Try.success(format) + } +} + +/** + * Sniffs a Lightweight Packaging Format (LPF). + * + * References: + * - https://www.w3.org/TR/lpf/ + * - https://www.w3.org/TR/pub-manifest/ + */ +public object LpfSniffer : ContentSniffer { + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + if ( + hints.hasFileExtension("lpf") || + hints.hasMediaType("application/lpf+zip") + ) { + return Format.LPF + } + + return format + } + + override suspend fun sniffContainer( + format: Format?, + container: Container + ): Try { + // Recognize exploded LPFs. + if (format?.conformsTo(Format.ZIP) == false) { + return Try.success(format) + } + + if (RelativeUrl("index.html")!! in container) { + return Try.success(Format.LPF) + } + + // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. + container[RelativeUrl("publication.json")!!] + ?.read() + ?.getOrElse { return Try.failure(it) } + ?.let { tryOrNull { String(it) } } + ?.let { manifest -> + if ( + manifest.contains("@context") && + manifest.contains("https://www.w3.org/ns/pub-context") + ) { + return Try.success(Format.LPF) + } + } + + return Try.success(format) + } +} + +/** + * Sniffs a RAR archive. + * + * At the moment, only hints are supported. + */ +public object RarSniffer : ContentSniffer { + + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + if ( + hints.hasFileExtension("rar") || + hints.hasMediaType("application/vnd.rar") || + hints.hasMediaType("application/x-rar") || + hints.hasMediaType("application/x-rar-compressed") + ) { + return Format.RAR + } + + return format + } +} + +/** + * Sniffs a ZIP archive. + */ +public object ZipSniffer : ContentSniffer { + + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + if (hints.hasMediaType("application/zip") || + hints.hasFileExtension("zip") + ) { + return Format.ZIP + } + + return format + } +} + +/** + * Sniffs a simple Archive-based publication format, like Comic Book Archive or Zipped Audio Book. + * + * Reference: https://wiki.mobileread.com/wiki/CBR_and_CBZ + */ +public object ArchiveSniffer : ContentSniffer { + + /** + * Authorized extensions for resources in a CBZ archive. + * Reference: https://wiki.mobileread.com/wiki/CBR_and_CBZ + */ + private val cbzExtensions = listOf( + // bitmap + "bmp", "dib", "gif", "jif", "jfi", "jfif", "jpg", "jpeg", "png", "tif", "tiff", "webp", + // metadata + "acbf", "xml" + ) + + /** + * Authorized extensions for resources in a ZAB archive (Zipped Audio Book). + */ + private val zabExtensions = listOf( + // audio + "aac", + "aiff", + "alac", + "flac", + "m4a", + "m4b", + "mp3", + "ogg", + "oga", + "mogg", + "opus", + "wav", + "webm", + // playlist + "asx", + "bio", + "m3u", + "m3u8", + "pla", + "pls", + "smil", + "vlc", + "wpl", + "xspf", + "zpl" + ) + + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + if ( + hints.hasFileExtension("cbz") || + hints.hasMediaType( + "application/vnd.comicbook+zip", + "application/x-cbz" + ) + ) { + return Format.CBZ + } + + if ( + hints.hasFileExtension("cbr") || + hints.hasMediaType("application/vnd.comicbook-rar") || + hints.hasMediaType("application/x-cbr") + ) { + return Format.CBR + } + + if (hints.hasFileExtension("zab")) { + return Format.ZAB + } + + return format + } + + override suspend fun sniffContainer( + format: Format?, + container: Container + ): Try { + fun isIgnored(url: Url): Boolean = + url.filename?.startsWith(".") == true || url.filename == "Thumbs.db" + + fun archiveContainsOnlyExtensions(fileExtensions: List): Boolean = + container.all { url -> + isIgnored(url) || url.extension?.let { + fileExtensions.contains( + it.lowercase(Locale.ROOT) + ) + } == true + } + + if ( + archiveContainsOnlyExtensions(cbzExtensions) && + format?.conformsTo(Format.ZIP) != false // Recognize exploded CBZ/CBR + ) { + return Try.success(Format.CBZ) + } + + if ( + archiveContainsOnlyExtensions(cbzExtensions) && + format?.conformsTo(Format.RAR) == true + ) { + return Try.success(Format.CBR) + } + + if ( + archiveContainsOnlyExtensions(zabExtensions) && + format?.conformsTo(Format.ZIP) != false // Recognize exploded ZAB + ) { + return Try.success(Format.ZAB) + } + + return Try.success(format) + } +} + +/** + * Sniffs a PDF document. + * + * Reference: https://www.loc.gov/preservation/digital/formats/fdd/fdd000123.shtml + */ +public object PdfSniffer : ContentSniffer { + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + if ( + hints.hasFileExtension("pdf") || + hints.hasMediaType("application/pdf") + ) { + return Format.PDF + } + + return format + } + + override suspend fun sniffBlob( + format: Format?, + source: Readable + ): Try { + source.read(0L until 5L) + .getOrElse { return Try.failure(it) } + .let { tryOrNull { it.toString(Charsets.UTF_8) } } + .takeIf { it == "%PDF-" } + ?.let { return Try.success(Format.PDF) } + + return Try.success(format) + } +} + +/** Sniffs a JSON document. */ +public object JsonSniffer : ContentSniffer { + override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? { + if (hints.hasFileExtension("json") || + hints.hasMediaType("application/json")) { + return Format.JSON + } + + if (hints.hasMediaType("application/problem+json")) { + return Format.JSON_PROBLEM_DETAILS + } + + return format + } + + override suspend fun sniffBlob( + format: Format?, + source: Readable + ): Try { + if (!source.canReadWholeBlob()) { + return Try.success(format) + } + + source.readDecodeOrElse( + decode = { it.decodeJson() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { null } + )?.let { return Try.success(Format.JSON) } + + return Try.success(format) + } +} + +/** + * Sniffs Adept protection on EPUBs. + */ +public object AdeptSniffer : ContentSniffer { + + override suspend fun sniffContainer( + format: Format?, + container: Container + ): Try { + if (format?.conformsTo(Format.EPUB) != true) { + return Try.success(format) + } + + container[Url("META-INF/encryption.xml")!!] + ?.readDecodeOrElse( + decode = { it.decodeXml() }, + recover = { null } + ) + ?.get("EncryptedData", EpubEncryption.ENC) + ?.flatMap { it.get("KeyInfo", EpubEncryption.SIG) } + ?.flatMap { it.get("resource", "http://ns.adobe.com/adept") } + ?.takeIf { it.isNotEmpty() } + ?.let { return Try.success(Format.EPUB_ADEPT) } + + container[Url("META-INF/rights.xml")!!] + ?.readDecodeOrElse( + decode = { it.decodeXml() }, + recover = { null } + ) + ?.takeIf { it.namespace == "http://ns.adobe.com/adept" } + ?.let { return Try.success(Format.EPUB_ADEPT) } + + return Try.success(format) + } +} + +/** + * Sniffs LCP protected packages. + */ +public object LcpSniffer : ContentSniffer { + + override suspend fun sniffContainer( + format: Format?, + container: Container + ): Try { + when { + format?.conformsTo(Format.RPF) == true -> { + val isLcpProtected = RelativeUrl("license.lcpl")!! in container || + hasLcpSchemeInManifest(container) + .getOrElse { return Try.failure(it) } + + if (isLcpProtected) { + val newFormat = when (format) { + Format.RPF_IMAGE -> Format.RPF_IMAGE_LCP + Format.RPF_AUDIO -> Format.RPF_AUDIO_LCP + Format.RPF_PDF -> Format.RPF_PDF_LCP + Format.RPF -> Format.RPF_LCP + else -> null + } + newFormat?.let { return Try.success(it) } + } + } + + format?.conformsTo(Format.EPUB) == true -> { + val isLcpProtected = RelativeUrl("META-INF/license.lcpl")!! in container || + hasLcpSchemeInEncryptionXml(container) + .getOrElse { return Try.failure(it) } + + if (isLcpProtected) { + return Try.success(Format.EPUB_LCP) + } + } + } + + return Try.success(format) + } + + private suspend fun hasLcpSchemeInManifest(container: Container): Try { + val manifest = container[Url("manifest.json")!!] + ?.readDecodeOrElse( + decode = { it.decodeRwpm() }, + recoverRead = { return Try.success(false) }, + recoverDecode = { return Try.success(false) } + ) ?: return Try.success(false) + + val manifestHasLcpScheme = manifest + .readingOrder + .any { it.properties.encryption?.scheme == "http://readium.org/2014/01/lcp" } + + return Try.success(manifestHasLcpScheme) + } + + private suspend fun hasLcpSchemeInEncryptionXml(container: Container): Try { + val encryptionXml = container[Url("META-INF/encryption.xml")!!] + ?.readDecodeOrElse( + decode = { it.decodeXml() }, + recover = { return Try.failure(it) } + ) ?: return Try.success(false) + + val hasLcpScheme = encryptionXml + .get("EncryptedData", EpubEncryption.ENC) + .flatMap { it.get("KeyInfo", EpubEncryption.SIG) } + .flatMap { it.get("RetrievalMethod", EpubEncryption.SIG) } + .any { it.getAttr("URI") == "license.lcpl#/encryption/content_key" } + + return Try.success(hasLcpScheme) + } +} + +private suspend fun Readable.canReadWholeBlob() = + length().getOrDefault(0) < 5 * 1000 * 1000 + +/** + * Returns whether the content is a JSON object containing all of the given root keys. + */ +@Suppress("SameParameterValue") +private suspend fun Readable.containsJsonKeys( + vararg keys: String +): Try { + val json = readDecodeOrElse( + decode = { it.decodeJson() }, + recoverRead = { return Try.failure(it) }, + recoverDecode = { return Try.success(false) } + ) + return Try.success(json.keys().asSequence().toSet().containsAll(keys.toList())) +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/FormatHints.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/FormatHints.kt new file mode 100644 index 0000000000..82a8457b12 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/FormatHints.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.sniff + +import java.nio.charset.Charset +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.mediatype.MediaType + +/** + * Bundle of media type and file extension hints for the [FormatHintsSniffer]. + */ +public data class FormatHints( + val format: Format? = null, + val mediaTypes: List = emptyList(), + val fileExtensions: List = emptyList() +) { + public companion object { + public operator fun invoke(mediaType: MediaType? = null, fileExtension: String? = null): FormatHints = + FormatHints( + mediaTypes = listOfNotNull(mediaType), + fileExtensions = listOfNotNull(fileExtension) + ) + + public operator fun invoke( + mediaTypes: List = emptyList(), + fileExtensions: List = emptyList() + ): FormatHints = + FormatHints( + mediaTypes = mediaTypes.mapNotNull { MediaType(it) }, + fileExtensions = fileExtensions + ) + } + + public operator fun plus(other: FormatHints): FormatHints = + FormatHints( + mediaTypes = mediaTypes + other.mediaTypes, + fileExtensions = fileExtensions + other.fileExtensions + ) + + /** + * Returns a new [FormatHints] after appending the given [fileExtension] hint. + */ + public fun addFileExtension(fileExtension: String?): FormatHints { + fileExtension ?: return this + return copy(fileExtensions = fileExtensions + fileExtension) + } + + /** Finds the first [Charset] declared in the media types' `charset` parameter. */ + public val charset: Charset? get() = + mediaTypes.firstNotNullOfOrNull { it.charset } + + /** Returns whether this context has any of the given file extensions, ignoring case. */ + public fun hasFileExtension(vararg fileExtensions: String): Boolean { + val fileExtensionsHints = this.fileExtensions.map { it.lowercase() } + for (fileExtension in fileExtensions.map { it.lowercase() }) { + if (fileExtensionsHints.contains(fileExtension)) { + return true + } + } + return false + } + + /** + * Returns whether this context has any of the given media type, ignoring case and extra + * parameters. + * + * Implementation note: Use [MediaType] to handle the comparison to avoid edge cases. + */ + public fun hasMediaType(vararg mediaTypes: String): Boolean { + @Suppress("NAME_SHADOWING") + val mediaTypes = mediaTypes.mapNotNull { MediaType(it) } + for (mediaType in mediaTypes) { + if (this.mediaTypes.any { mediaType.contains(it) }) { + return true + } + } + return false + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index 7ae50ac0da..9a74b69d11 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -14,36 +14,37 @@ import java.util.zip.ZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.archive.ArchiveFactory +import org.readium.r2.shared.util.asset.ArchiveOpener +import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.file.FileSystemError +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.asset.SniffError /** - * An [ArchiveFactory] to open local ZIP files with Java's [ZipFile]. + * An [ArchiveOpener] to open local ZIP files with Java's [ZipFile]. */ internal class FileZipArchiveProvider { - suspend fun sniffFile(file: File): Try { + suspend fun sniff(file: File): Try { return withContext(Dispatchers.IO) { try { - FileZipContainer(ZipFile(file), file) - Try.success(MediaType.ZIP) + val container = FileZipContainer(ZipFile(file), file) + Try.success(ContainerAsset(Format.ZIP, container)) } catch (e: ZipException) { - Try.failure(MediaTypeSnifferError.NotRecognized) + Try.failure(SniffError.NotRecognized) } catch (e: SecurityException) { Try.failure( - MediaTypeSnifferError.Reading( + SniffError.Reading( ReadError.Access(FileSystemError.Forbidden(e)) ) ) } catch (e: IOException) { Try.failure( - MediaTypeSnifferError.Reading( + SniffError.Reading( ReadError.Access(FileSystemError.IO(e)) ) ) @@ -51,13 +52,13 @@ internal class FileZipArchiveProvider { } } - suspend fun create( - mediaType: MediaType, + suspend fun open( + format: Format, file: File - ): Try, ArchiveFactory.CreateError> { - if (mediaType != MediaType.ZIP) { + ): Try, ArchiveOpener.OpenError> { + if (!format.conformsTo(Format.ZIP)) { return Try.failure( - ArchiveFactory.CreateError.FormatNotSupported(mediaType) + ArchiveOpener.OpenError.FormatNotSupported(format) ) } @@ -68,32 +69,32 @@ internal class FileZipArchiveProvider { } // Internal for testing purpose - internal suspend fun open(file: File): Try, ArchiveFactory.CreateError> = + internal suspend fun open(file: File): Try, ArchiveOpener.OpenError> = withContext(Dispatchers.IO) { try { val archive = FileZipContainer(ZipFile(file), file) Try.success(archive) } catch (e: FileNotFoundException) { Try.failure( - ArchiveFactory.CreateError.Reading( + ArchiveOpener.OpenError.Reading( ReadError.Access(FileSystemError.FileNotFound(e)) ) ) } catch (e: ZipException) { Try.failure( - ArchiveFactory.CreateError.Reading( + ArchiveOpener.OpenError.Reading( ReadError.Decoding(e) ) ) } catch (e: SecurityException) { Try.failure( - ArchiveFactory.CreateError.Reading( + ArchiveOpener.OpenError.Reading( ReadError.Access(FileSystemError.Forbidden(e)) ) ) } catch (e: IOException) { Try.failure( - ArchiveFactory.CreateError.Reading( + ArchiveOpener.OpenError.Reading( ReadError.Access(FileSystemError.IO(e)) ) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt index 65a6ddb808..5450839442 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt @@ -20,15 +20,14 @@ import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.archive.ArchiveProperties -import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.ArchiveProperties import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.toUrl internal class FileZipContainer( @@ -127,8 +126,6 @@ internal class FileZipContainer( } } - override val archiveMediaType: MediaType = MediaType.ZIP - override val sourceUrl: AbsoluteUrl = file.toUrl() override val entries: Set = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 57468b3a16..3d848eed1b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -13,56 +13,57 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.extensions.findInstance import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.archive.ArchiveFactory +import org.readium.r2.shared.util.asset.ArchiveOpener +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.data.Readable -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel /** - * An [ArchiveFactory] able to open a ZIP archive served through a stream (e.g. HTTP server, + * An [ArchiveOpener] able to open a ZIP archive served through a stream (e.g. HTTP server, * content URI, etc.). */ internal class StreamingZipArchiveProvider { - suspend fun sniffBlob(readable: Readable): Try { + suspend fun sniffOpen(source: Readable): Try { return try { - openBlob(readable, ::ReadException, null) - Try.success(MediaType.ZIP) + val container = openBlob(source, ::ReadException, null) + Try.success(ContainerAsset(Format.ZIP, container)) } catch (exception: Exception) { exception.findInstance(ReadException::class.java) - ?.let { Try.failure(MediaTypeSnifferError.Reading(it.error)) } - ?: Try.failure(MediaTypeSnifferError.NotRecognized) + ?.let { Try.failure(SniffError.Reading(it.error)) } + ?: Try.failure(SniffError.NotRecognized) } } - suspend fun create( - mediaType: MediaType, - readable: Readable - ): Try, ArchiveFactory.CreateError> { - if (mediaType != MediaType.ZIP) { + suspend fun open( + format: Format, + source: Readable + ): Try, ArchiveOpener.OpenError> { + if (!format.conformsTo(Format.ZIP)) { return Try.failure( - ArchiveFactory.CreateError.FormatNotSupported(mediaType) + ArchiveOpener.OpenError.FormatNotSupported(format) ) } return try { val container = openBlob( - readable, + source, ::ReadException, - (readable as? Resource)?.sourceUrl + (source as? Resource)?.sourceUrl ) Try.success(container) } catch (exception: Exception) { exception.findInstance(ReadException::class.java) - ?.let { Try.failure(ArchiveFactory.CreateError.Reading(it.error)) } - ?: Try.failure(ArchiveFactory.CreateError.Reading(ReadError.Decoding(exception))) + ?.let { Try.failure(ArchiveOpener.OpenError.Reading(it.error)) } + ?: Try.failure(ArchiveOpener.OpenError.Reading(ReadError.Decoding(exception))) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt index fa2332b585..730cdfae23 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt @@ -16,16 +16,15 @@ import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.archive.ArchiveProperties -import org.readium.r2.shared.util.archive.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.io.CountingInputStream -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.ArchiveProperties import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.resource.filename import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipArchiveEntry import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile @@ -137,8 +136,6 @@ internal class StreamingZipContainer( } } - override val archiveMediaType: MediaType = MediaType.ZIP - override val entries: Set = zipFile.entries.toList() .filterNot { it.isDirectory } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt deleted file mode 100644 index 7e02aa7057..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveFactory.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.zip - -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.archive.ArchiveFactory -import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.Readable -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.Resource - -public class ZipArchiveFactory : ArchiveFactory { - - private val fileZipArchiveProvider = FileZipArchiveProvider() - - private val streamingZipArchiveProvider = StreamingZipArchiveProvider() - - override suspend fun create( - mediaType: MediaType, - source: Readable - ): Try, ArchiveFactory.CreateError> = - (source as? Resource)?.sourceUrl?.toFile() - ?.let { fileZipArchiveProvider.create(mediaType, it) } - ?: streamingZipArchiveProvider.create(mediaType, source) -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt new file mode 100644 index 0000000000..2786e76063 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.zip + +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.ArchiveOpener +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.Readable +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.asset.SniffError + +public class ZipArchiveOpener : ArchiveOpener { + + private val fileZipArchiveProvider = FileZipArchiveProvider() + + private val streamingZipArchiveProvider = StreamingZipArchiveProvider() + + override suspend fun open( + format: Format, + source: Readable + ): Try, ArchiveOpener.OpenError> = + (source as? Resource)?.sourceUrl?.toFile() + ?.let { fileZipArchiveProvider.open(format, it) } + ?: streamingZipArchiveProvider.open(format, source) + + override suspend fun sniffOpen(source: Readable): Try { + (source as? Resource)?.sourceUrl?.toFile() + ?.let { return fileZipArchiveProvider.sniff(it) } + + return streamingZipArchiveProvider.sniffOpen(source) + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt deleted file mode 100644 index 1bf9c12d00..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipMediaTypeSniffer.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.zip - -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Readable -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeHints -import org.readium.r2.shared.util.mediatype.MediaTypeSniffer -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError -import org.readium.r2.shared.util.resource.Resource - -/** - * Sniffs a ZIP archive. - */ -public object ZipMediaTypeSniffer : MediaTypeSniffer { - - private val fileZipArchiveProvider = FileZipArchiveProvider() - - private val streamingZipArchiveProvider = StreamingZipArchiveProvider() - - override fun sniffHints(hints: MediaTypeHints): Try { - if (hints.hasMediaType("application/zip") || - hints.hasFileExtension("zip") - ) { - return Try.success(MediaType.ZIP) - } - - return Try.failure(MediaTypeSnifferError.NotRecognized) - } - - override suspend fun sniffBlob(source: Readable): Try { - (source as? Resource)?.sourceUrl?.toFile() - ?.let { return fileZipArchiveProvider.sniffFile(it) } - - return streamingZipArchiveProvider.sniffBlob(source) - } -} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt index 619c464853..0562a20e98 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt @@ -3,16 +3,18 @@ package org.readium.r2.shared.util.mediatype import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking import org.junit.Test +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatRegistry class FormatRegistryTest { private fun sut() = FormatRegistry() @Test - fun `get known file extension from canonical media type`() = runBlocking { + fun `get known file extension from format`() = runBlocking { assertEquals( "epub", - sut().fileExtension(MediaType.EPUB) + sut()[Format.EPUB]?.fileExtension.value ) } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt index b9023a8d0b..38582722e6 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt @@ -8,8 +8,10 @@ import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures import org.readium.r2.shared.util.checkSuccess +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.resource.StringResource -import org.readium.r2.shared.util.zip.ZipArchiveFactory +import org.readium.r2.shared.util.sniff.SniffError +import org.readium.r2.shared.util.zip.ZipArchiveOpener import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf @@ -21,7 +23,7 @@ class MediaTypeRetrieverTest { private val retriever = MediaTypeRetriever( DefaultMediaTypeSniffer(), FormatRegistry(), - ZipArchiveFactory() + ZipArchiveOpener() ) @Test @@ -92,7 +94,7 @@ class MediaTypeRetrieverTest { assertNull(retriever.retrieve(mediaType = "invalid")) assertEquals( retriever.retrieve(fixtures.fileAt("unknown")).failureOrNull(), - MediaTypeSnifferError.NotRecognized + SniffError.NotRecognized ) } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt index befffd96b7..901ee8efde 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/PropertiesTest.kt @@ -6,8 +6,6 @@ import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.assertJSONEquals -import org.readium.r2.shared.util.archive.ArchiveProperties -import org.readium.r2.shared.util.archive.archive import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt index 9966234031..8074fce11a 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -22,7 +22,7 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.file.DirectoryContainer -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.use import org.readium.r2.shared.util.zip.FileZipArchiveProvider import org.readium.r2.shared.util.zip.StreamingZipArchiveProvider @@ -42,8 +42,8 @@ class ZipContainerTest(val sut: suspend () -> Container) { val zipArchive = suspend { assertNotNull( FileZipArchiveProvider() - .create( - mediaType = MediaType.ZIP, + .open( + format = Format.EPUB, file = File(epubZip.path) ) .getOrNull() diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index a79dad7d9c..afb4c4e03a 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -19,10 +19,10 @@ import org.readium.r2.shared.util.data.CompositeContainer import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.decodeRwpm import org.readium.r2.shared.util.data.readDecodeOrElse +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpContainer -import org.readium.r2.shared.util.mediatype.FormatRegistry -import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.streamer.parser.PublicationParser import timber.log.Timber @@ -62,7 +62,7 @@ internal class ParserAssetFactory( ): Try = Try.success( PublicationParser.Asset( - mediaType = asset.mediaType, + format = asset.format, container = asset.container ) ) @@ -70,7 +70,7 @@ internal class ParserAssetFactory( private suspend fun createParserAssetForResource( asset: ResourceAsset ): Try = - if (asset.mediaType.isRwpm) { + if (asset.format.conformsTo(Format.RWPM)) { createParserAssetForManifest(asset) } else { createParserAssetForContent(asset) @@ -120,7 +120,7 @@ internal class ParserAssetFactory( return Try.success( PublicationParser.Asset( - mediaType = MediaType.READIUM_WEBPUB, + format = Format.RPF, container = container ) ) @@ -133,7 +133,9 @@ internal class ParserAssetFactory( // HREF "/$assetName". This was fragile if the asset named changed, or was different on // other devices. To avoid this, we now use a single link with the HREF // "publication.extension". - val extension = formatRegistry.fileExtension(asset.mediaType)?.addPrefix(".") ?: "" + val extension = formatRegistry[asset.format] + ?.fileExtension?.value?.addPrefix(".") + ?: "" val container = SingleResourceContainer( Url("publication$extension")!!, asset.resource @@ -141,7 +143,7 @@ internal class ParserAssetFactory( return Try.success( PublicationParser.Asset( - mediaType = asset.mediaType, + format = asset.format, container = container ) ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 433ee2bf7f..0578cc854c 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -16,15 +16,13 @@ import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.AssetSniffer +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.logging.WarningLogger -import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.FormatRegistry -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.pdf.PdfDocumentFactory -import org.readium.r2.shared.util.zip.ZipArchiveFactory import org.readium.r2.streamer.parser.PublicationParser import org.readium.r2.streamer.parser.audio.AudioParser import org.readium.r2.streamer.parser.epub.EpubParser @@ -58,7 +56,7 @@ public class PublicationFactory( formatRegistry: FormatRegistry, private val httpClient: HttpClient, pdfFactory: PdfDocumentFactory<*>?, - private val mediaTypeRetriever: MediaTypeRetriever, + assetSniffer: AssetSniffer, private val onCreatePublication: Publication.Builder.() -> Unit = {} ) { public sealed class OpenError( @@ -71,62 +69,24 @@ public class PublicationFactory( ) : OpenError("An error occurred while trying to read asset.", cause) public class FormatNotSupported( - override val cause: Error? - ) : OpenError("Asset is not supported.", cause) - - public class ContentProtectionNotSupported( override val cause: Error? = null - ) : OpenError("No ContentProtection available to open asset.", cause) - } - - public companion object { - public operator fun invoke( - context: Context, - contentProtections: List = emptyList(), - onCreatePublication: Publication.Builder.() -> Unit - ): PublicationFactory { - val mediaTypeSniffer = - DefaultMediaTypeSniffer() - - val archiveFactory = - ZipArchiveFactory() - - val formatRegistry = - FormatRegistry() - - val mediaTypeRetriever = - MediaTypeRetriever( - mediaTypeSniffer, - FormatRegistry(), - archiveFactory - ) - - return PublicationFactory( - context = context, - contentProtections = contentProtections, - mediaTypeRetriever = mediaTypeRetriever, - formatRegistry = formatRegistry, - httpClient = DefaultHttpClient(), - pdfFactory = null, - onCreatePublication = onCreatePublication - ) - } + ) : OpenError("Asset is not supported.", cause) } - private val contentProtections: Map = + private val contentProtections: List = buildList { add(LcpFallbackContentProtection()) add(AdeptFallbackContentProtection()) addAll(contentProtections.asReversed()) - }.associateBy(ContentProtection::scheme) + } private val defaultParsers: List = listOfNotNull( EpubParser(), pdfFactory?.let { PdfParser(context, it) }, ReadiumWebPubParser(context, httpClient, pdfFactory), - ImageParser(mediaTypeRetriever), - AudioParser(mediaTypeRetriever) + ImageParser(assetSniffer, formatRegistry), + AudioParser(assetSniffer, formatRegistry) ) private val parsers: List = parsers + @@ -148,8 +108,6 @@ public class PublicationFactory( * issues. * * @param asset Digital medium (e.g. a file) used to access the publication. - * @param contentProtectionScheme Scheme of the [ContentProtection] protecting the publication, - * or null if there is none. * @param credentials Credentials that Content Protections can use to attempt to unlock a * publication, for example a password. * @param allowUserInteraction Indicates whether the user can be prompted, for example for its @@ -162,7 +120,6 @@ public class PublicationFactory( */ public suspend fun open( asset: Asset, - contentProtectionScheme: ContentProtection.Scheme? = null, credentials: String? = null, allowUserInteraction: Boolean, onCreatePublication: Publication.Builder.() -> Unit = {}, @@ -173,73 +130,48 @@ public class PublicationFactory( onCreatePublication(this) } - return if (contentProtectionScheme == null) { - openUnprotected( - asset, - compositeOnCreatePublication, - warnings - ) - } else { - openProtected( - asset, - contentProtectionScheme, - credentials, - allowUserInteraction, - compositeOnCreatePublication, - warnings - ) - } - } - - private suspend fun openUnprotected( - asset: Asset, - onCreatePublication: Publication.Builder.() -> Unit, - warnings: WarningLogger? - ): Try { - val parserAsset = parserAssetFactory.createParserAsset(asset) - .mapFailure { + parserAssetFactory.createParserAsset(asset) + .getOrElse { when (it) { is ParserAssetFactory.CreateError.Reading -> - OpenError.Reading(it.cause) + return Try.failure(OpenError.Reading(it.cause)) is ParserAssetFactory.CreateError.FormatNotSupported -> - OpenError.FormatNotSupported(it.cause) + null } } - .getOrElse { return Try.failure(it) } - return openParserAsset(parserAsset, onCreatePublication, warnings) - } - - private suspend fun openProtected( - asset: Asset, - contentProtectionScheme: ContentProtection.Scheme, - credentials: String?, - allowUserInteraction: Boolean, - onCreatePublication: Publication.Builder.() -> Unit, - warnings: WarningLogger? - ): Try { - val protectedAsset = contentProtections[contentProtectionScheme] - ?.open(asset, credentials, allowUserInteraction) - ?.mapFailure { - when (it) { - is ContentProtection.OpenError.Reading -> - OpenError.Reading(it.cause) - is ContentProtection.OpenError.AssetNotSupported -> - OpenError.FormatNotSupported(it) + ?.let { openParserAsset(it, compositeOnCreatePublication, warnings) } + + for (protection in contentProtections) { + protection.open(asset, credentials, allowUserInteraction) + .getOrElse { + when (it) { + is ContentProtection.OpenError.Reading -> + return Try.failure(OpenError.Reading(it.cause)) + is ContentProtection.OpenError.AssetNotSupported -> + null + } + }?.let { protectedAsset -> + val parserAsset = PublicationParser.Asset( + protectedAsset.format, + protectedAsset.container + ) + + val fullOnCreatePublication: Publication.Builder.() -> Unit = { + protectedAsset.onCreatePublication.invoke(this) + onCreatePublication(this) + } + + return openParserAsset(parserAsset, fullOnCreatePublication) } - } - ?.getOrElse { return Try.failure(it) } - ?: return Try.failure(OpenError.ContentProtectionNotSupported()) - - val parserAsset = PublicationParser.Asset( - protectedAsset.mediaType, - protectedAsset.container - ) + } - val compositeOnCreatePublication: Publication.Builder.() -> Unit = { - protectedAsset.onCreatePublication.invoke(this) - onCreatePublication(this) + if (asset !is ContainerAsset) { + return Try.failure(OpenError.FormatNotSupported()) } + val parserAsset = PublicationParser.Asset(asset.format, asset.container) + + return openParserAsset(parserAsset, compositeOnCreatePublication, warnings) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt index 8a49f8d1ad..c7bb47c021 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt @@ -6,12 +6,11 @@ package org.readium.r2.streamer.parser -import kotlin.String import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.logging.WarningLogger -import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource /** @@ -22,13 +21,13 @@ public interface PublicationParser { /** * Full publication asset. * - * @param mediaType Media type of the "virtual" publication asset, built from the source asset. - * For example, if the source asset was a `application/audiobook+json`, the "virtual" asset + * @param format Format of the "virtual" publication asset, built from the source asset. + * For example, if the source asset media type was a `application/audiobook+json`, the "virtual" asset * media type will be `application/audiobook+zip`. * @param container Container granting access to the resources of the publication. */ public data class Asset( - val mediaType: MediaType, + val format: Format, val container: Container ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index d65b7079e1..0729e19dd9 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -14,12 +14,13 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger -import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs @@ -32,19 +33,20 @@ import org.readium.r2.streamer.parser.PublicationParser * It can also work for a standalone audio file. */ public class AudioParser( - private val mediaTypeRetriever: MediaTypeRetriever + private val assetSniffer: AssetSniffer, + private val formatRegistry: FormatRegistry ) : PublicationParser { override suspend fun parse( asset: PublicationParser.Asset, warnings: WarningLogger? ): Try { - if (!asset.mediaType.matches(MediaType.ZAB) && !asset.mediaType.isAudio) { + if (!asset.format.conformsTo(Format.ZAB) && formatRegistry[asset.format]?.mediaType?.isAudio != true) { return Try.failure(PublicationParser.Error.FormatNotSupported()) } val readingOrder = - if (asset.mediaType.matches(MediaType.ZAB)) { + if (asset.format.conformsTo(Format.ZAB)) { asset.container .filter { zabCanContain(it) } .sortedBy { it.toString() } @@ -66,12 +68,13 @@ public class AudioParser( val readingOrderLinks = readingOrder.map { url -> val mediaType = asset.container[url]!!.use { resource -> - mediaTypeRetriever.retrieve(resource) + assetSniffer.sniff(resource) + .map { formatRegistry[it]?.mediaType } .getOrElse { error -> when (error) { - MediaTypeSnifferError.NotRecognized -> + SniffError.NotRecognized -> null - is MediaTypeSnifferError.Reading -> + is SniffError.Reading -> return Try.failure(PublicationParser.Error.Reading(error.cause)) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 6e6f9a596b..c0a4939267 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -24,6 +24,7 @@ import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.data.decodeXml import org.readium.r2.shared.util.data.readDecodeOrElse import org.readium.r2.shared.util.data.readDecodeOrNull +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType @@ -49,7 +50,7 @@ public class EpubParser( asset: PublicationParser.Asset, warnings: WarningLogger? ): Try { - if (asset.mediaType != MediaType.EPUB) { + if (!asset.format.conformsTo(Format.EPUB)) { return Try.failure(PublicationParser.Error.FormatNotSupported()) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt index 3fb4c54b8a..af2a7af69c 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt @@ -17,7 +17,7 @@ import org.readium.r2.shared.publication.presentation.Presentation import org.readium.r2.shared.publication.presentation.presentation import org.readium.r2.shared.publication.services.PositionsService import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.archive.archive +import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index 8d27bbf22e..84ded4f5d2 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -15,12 +15,14 @@ import org.readium.r2.shared.publication.services.PerResourcePositionsService import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever -import org.readium.r2.shared.util.mediatype.MediaTypeSnifferError +import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs @@ -33,19 +35,20 @@ import org.readium.r2.streamer.parser.PublicationParser * It can also work for a standalone bitmap file. */ public class ImageParser( - private val mediaTypeRetriever: MediaTypeRetriever + private val assetSniffer: AssetSniffer, + private val formatRegistry: FormatRegistry ) : PublicationParser { override suspend fun parse( asset: PublicationParser.Asset, warnings: WarningLogger? ): Try { - if (!asset.mediaType.matches(MediaType.CBZ) && !asset.mediaType.isBitmap) { + if (!asset.format.conformsTo(Format.CBZ) && formatRegistry[asset.format]?.mediaType?.isBitmap != true) { return Try.failure(PublicationParser.Error.FormatNotSupported()) } val readingOrder = - if (asset.mediaType.matches(MediaType.CBZ)) { + if (asset.format.conformsTo(Format.CBZ)) { (asset.container) .filter { cbzCanContain(it) } .sortedBy { it.toString() } @@ -65,12 +68,13 @@ public class ImageParser( val readingOrderLinks = readingOrder.map { url -> val mediaType = asset.container[url]!!.use { resource -> - mediaTypeRetriever.retrieve(resource) + assetSniffer.sniff(resource) + .map { formatRegistry[it]?.mediaType } .getOrElse { error -> when (error) { - MediaTypeSnifferError.NotRecognized -> + SniffError.NotRecognized -> null - is MediaTypeSnifferError.Reading -> + is SniffError.Reading -> return Try.failure(PublicationParser.Error.Reading(error.cause)) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index f9e8f9faa9..9243e5a04e 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -15,6 +15,7 @@ import org.readium.r2.shared.publication.services.InMemoryCoverService import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType @@ -38,7 +39,7 @@ public class PdfParser( asset: PublicationParser.Asset, warnings: WarningLogger? ): Try { - if (asset.mediaType != MediaType.PDF) { + if (!asset.format.conformsTo(Format.PDF)) { return Try.failure(PublicationParser.Error.FormatNotSupported()) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt index 72f773540e..61028847fb 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt @@ -109,7 +109,7 @@ internal class LcpdfPositionsService( } } - companion object { + companion object { fun create(pdfFactory: PdfDocumentFactory<*>): (Publication.Service.Context) -> LcpdfPositionsService = { serviceContext -> LcpdfPositionsService( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index af97d92e6e..20f84e73c5 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -20,6 +20,7 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.decodeRwpm import org.readium.r2.shared.util.data.readDecodeOrElse +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType @@ -40,7 +41,7 @@ public class ReadiumWebPubParser( asset: PublicationParser.Asset, warnings: WarningLogger? ): Try { - if (!asset.mediaType.isReadiumWebPublication) { + if (!asset.format.conformsTo(Format.RPF)) { return Try.failure(PublicationParser.Error.FormatNotSupported()) } @@ -62,7 +63,7 @@ public class ReadiumWebPubParser( // Checks the requirements from the LCPDF specification. // https://readium.org/lcp-specs/notes/lcp-for-pdf.html val readingOrder = manifest.readingOrder - if (asset.mediaType == MediaType.LCP_PROTECTED_PDF && + if (asset.format.conformsTo(Format.RPF_PDF_LCP) && (readingOrder.isEmpty() || !readingOrder.all { MediaType.PDF.matches(it.mediaType) }) ) { return Try.failure( @@ -75,17 +76,17 @@ public class ReadiumWebPubParser( val servicesBuilder = Publication.ServicesBuilder().apply { cacheServiceFactory = InMemoryCacheService.createFactory(context) - positionsServiceFactory = when (asset.mediaType) { - MediaType.LCP_PROTECTED_PDF -> + positionsServiceFactory = when (asset.format) { + Format.RPF_PDF_LCP -> pdfFactory?.let { LcpdfPositionsService.create(it) } - MediaType.DIVINA -> + Format.RPF_IMAGE -> PerResourcePositionsService.createFactory(MediaType("image/*")!!) else -> WebPositionsService.createFactory(httpClient) } - locatorServiceFactory = when (asset.mediaType) { - MediaType.READIUM_AUDIOBOOK, MediaType.LCP_PROTECTED_AUDIOBOOK -> + locatorServiceFactory = when { + asset.format.conformsTo(Format.RPF_AUDIO) -> AudioLocatorService.createFactory() else -> null @@ -97,11 +98,3 @@ public class ReadiumWebPubParser( } } -/** Returns whether this media type is of a Readium Web Publication profile. */ -private val MediaType.isReadiumWebPublication: Boolean get() = matchesAny( - MediaType.READIUM_WEBPUB, - MediaType.DIVINA, - MediaType.LCP_PROTECTED_PDF, - MediaType.READIUM_AUDIOBOOK, - MediaType.LCP_PROTECTED_AUDIOBOOK -) diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt index 719db9b7ce..b039879d43 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt @@ -21,8 +21,8 @@ import org.readium.r2.shared.publication.presentation.Presentation 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.archive.ArchiveProperties -import org.readium.r2.shared.util.archive.archive +import org.readium.r2.shared.util.resource.ArchiveProperties +import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.mediatype.MediaType diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index ca6ebe942f..4c386a77fb 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -19,15 +19,16 @@ import org.junit.runner.RunWith import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.firstWithRel import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.file.FileResource -import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.FormatRegistry +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.SingleResourceContainer +import org.readium.r2.shared.util.sniff.DefaultContentSniffer import org.readium.r2.shared.util.toUrl -import org.readium.r2.shared.util.zip.ZipArchiveFactory +import org.readium.r2.shared.util.zip.ZipArchiveOpener import org.readium.r2.streamer.parseBlocking import org.readium.r2.streamer.parser.PublicationParser import org.robolectric.RobolectricTestRunner @@ -35,27 +36,26 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class ImageParserTest { - private val mediaTypeRetriever = - MediaTypeRetriever( - DefaultMediaTypeSniffer(), - FormatRegistry(), - ZipArchiveFactory() - ) + private val archiveOpener = ZipArchiveOpener() + + private val assetSniffer = AssetSniffer(DefaultContentSniffer, archiveOpener) + + private val formatRegistry = FormatRegistry() - private val parser = ImageParser(mediaTypeRetriever) + private val parser = ImageParser(assetSniffer, formatRegistry) private val cbzAsset = runBlocking { val file = fileForResource("futuristic_tales.cbz") - val resource = FileResource(file, mediaType = MediaType.CBZ) - val archive = ZipArchiveFactory().create(MediaType.ZIP, resource).checkSuccess() - PublicationParser.Asset(mediaType = MediaType.CBZ, archive) + val resource = FileResource(file) + val archive = archiveOpener.open(Format.ZIP, resource).checkSuccess() + PublicationParser.Asset(format = Format.CBZ, archive) } private val jpgAsset = runBlocking { val file = fileForResource("futuristic_tales.jpg") val resource = FileResource(file, mediaType = MediaType.JPEG) PublicationParser.Asset( - mediaType = MediaType.JPEG, + format = Format.JPEG, SingleResourceContainer(file.toUrl(), resource) ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/Application.kt b/test-app/src/main/java/org/readium/r2/testapp/Application.kt index 641c3366d2..f3cf6be403 100755 --- a/test-app/src/main/java/org/readium/r2/testapp/Application.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Application.kt @@ -79,8 +79,8 @@ class Application : android.app.Application() { bookRepository, CoverStorage(storageDir, httpClient = readium.httpClient), readium.publicationFactory, - readium.assetRetriever, - readium.protectionRetriever, + readium.assetOpener, + readium.formatRegistry, createPublicationRetriever = { listener -> PublicationRetriever( listener = listener, @@ -89,7 +89,7 @@ class Application : android.app.Application() { listener = localListener, context = applicationContext, storageDir = storageDir, - assetRetriever = readium.assetRetriever, + assetOpener = readium.assetOpener, formatRegistry = readium.formatRegistry, createLcpPublicationRetriever = { lcpListener -> readium.lcpService.getOrNull()?.publicationRetriever() diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index a402d07402..c5b54cd8e2 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -14,20 +14,19 @@ import org.readium.r2.lcp.LcpService import org.readium.r2.lcp.auth.LcpDialogAuthentication import org.readium.r2.navigator.preferences.FontFamily import org.readium.r2.shared.ExperimentalReadiumApi -import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetriever import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.AssetOpener +import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.content.ContentResourceFactory import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager import org.readium.r2.shared.util.file.FileResourceFactory +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpResourceFactory -import org.readium.r2.shared.util.mediatype.DefaultMediaTypeSniffer -import org.readium.r2.shared.util.mediatype.FormatRegistry -import org.readium.r2.shared.util.mediatype.MediaTypeRetriever import org.readium.r2.shared.util.resource.CompositeResourceFactory -import org.readium.r2.shared.util.zip.ZipArchiveFactory +import org.readium.r2.shared.util.sniff.DefaultContentSniffer +import org.readium.r2.shared.util.zip.ZipArchiveOpener import org.readium.r2.streamer.PublicationFactory /** @@ -35,21 +34,14 @@ import org.readium.r2.streamer.PublicationFactory */ class Readium(context: Context) { - private val mediaTypeSniffer = - DefaultMediaTypeSniffer() - - private val archiveFactory = - ZipArchiveFactory() + private val archiveOpener = + ZipArchiveOpener() val formatRegistry = FormatRegistry() - private val mediaTypeRetriever = - MediaTypeRetriever( - mediaTypeSniffer, - formatRegistry, - archiveFactory - ) + private val assetSniffer = + AssetSniffer(DefaultContentSniffer, archiveOpener) val httpClient = DefaultHttpClient() @@ -59,17 +51,14 @@ class Readium(context: Context) { HttpResourceFactory(httpClient) ) - val assetRetriever = AssetRetriever( - mediaTypeRetriever, + val assetOpener = AssetOpener( + assetSniffer, resourceFactory, - archiveFactory, - formatRegistry + archiveOpener, ) val downloadManager = AndroidDownloadManager( context = context, - mediaTypeRetriever = mediaTypeRetriever, - formatRegistry = formatRegistry, destStorage = AndroidDownloadManager.Storage.App ) @@ -79,8 +68,8 @@ class Readium(context: Context) { */ val lcpService = LcpService( context, - assetRetriever, - mediaTypeRetriever, + assetOpener, + assetSniffer, downloadManager )?.let { Try.success(it) } ?: Try.failure(LcpError.Unknown(DebugError("liblcp is missing on the classpath"))) @@ -91,10 +80,6 @@ class Readium(context: Context) { lcpService.getOrNull()?.contentProtection(lcpDialogAuthentication) ) - val protectionRetriever = ContentProtectionSchemeRetriever( - contentProtections - ) - /** * The PublicationFactory is used to parse and open publications. */ @@ -102,7 +87,7 @@ class Readium(context: Context) { context, contentProtections = contentProtections, formatRegistry = formatRegistry, - mediaTypeRetriever = mediaTypeRetriever, + assetSniffer = assetSniffer, httpClient = httpClient, // Only required if you want to support PDF files using the PDFium adapter. pdfFactory = PdfiumDocumentFactory(context) diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt index ad8a8fa916..e7e85c0276 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt @@ -14,8 +14,8 @@ import org.joda.time.DateTime import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.indexOfFirstWithHref -import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.testapp.data.db.BooksDao import org.readium.r2.testapp.data.model.Book @@ -81,8 +81,8 @@ class BookRepository( suspend fun insertBook( url: Url, + format: Format, mediaType: MediaType, - drm: ContentProtection.Scheme?, publication: Publication, cover: File ): Long { @@ -92,8 +92,8 @@ class BookRepository( author = publication.metadata.authorName, href = url.toString(), identifier = publication.metadata.identifier ?: "", + format = format, mediaType = mediaType, - drm = drm, progression = "{}", cover = cover.path ) diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt b/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt index 14aca599ac..2ba585eb55 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt @@ -9,8 +9,8 @@ package org.readium.r2.testapp.data.model import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.mediatype.MediaType @Entity(tableName = Book.TABLE_NAME) @@ -32,8 +32,8 @@ data class Book( val progression: String? = null, @ColumnInfo(name = MEDIA_TYPE) val rawMediaType: String, - @ColumnInfo(name = DRM) - val drm: String? = null, + @ColumnInfo(name = FORMAT_ID) + val formatId: String, @ColumnInfo(name = COVER) val cover: String ) { @@ -46,8 +46,8 @@ data class Book( author: String? = null, identifier: String, progression: String? = null, + format: Format, mediaType: MediaType, - drm: ContentProtection.Scheme?, cover: String ) : this( id = id, @@ -57,18 +57,18 @@ data class Book( author = author, identifier = identifier, progression = progression, + formatId = format.id, rawMediaType = mediaType.toString(), - drm = drm?.uri, cover = cover ) val url: AbsoluteUrl get() = AbsoluteUrl(href)!! val mediaType: MediaType get() = - MediaType(rawMediaType) ?: MediaType.BINARY + MediaType(rawMediaType)!! - val drmScheme: ContentProtection.Scheme? get() = - drm?.let { ContentProtection.Scheme(it) } + val format: Format get() = + Format(formatId) companion object { @@ -82,6 +82,6 @@ data class Book( const val PROGRESSION = "progression" const val MEDIA_TYPE = "media_type" const val COVER = "cover" - const val DRM = "drm" + const val FORMAT_ID = "format_id" } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index c70d2c4340..cecfef70aa 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -13,16 +13,15 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetriever import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.AssetRetriever -import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.asset.AssetOpener import org.readium.r2.shared.util.file.FileSystemError +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.toUrl -import org.readium.r2.shared.util.tryRecover import org.readium.r2.streamer.PublicationFactory import org.readium.r2.testapp.data.BookRepository import org.readium.r2.testapp.data.model.Book @@ -40,8 +39,8 @@ class Bookshelf( private val bookRepository: BookRepository, private val coverStorage: CoverStorage, private val publicationFactory: PublicationFactory, - private val assetRetriever: AssetRetriever, - private val protectionRetriever: ContentProtectionSchemeRetriever, + private val assetOpener: AssetOpener, + private val formatRegistry: FormatRegistry, createPublicationRetriever: (PublicationRetriever.Listener) -> PublicationRetriever ) { val channel: Channel = @@ -126,47 +125,30 @@ class Bookshelf( coverUrl: AbsoluteUrl? = null ): Try { val asset = - assetRetriever.retrieve(url) + assetOpener.open(url) .getOrElse { return Try.failure( ImportError.Publication(PublicationError(it)) ) } - val drmScheme = - protectionRetriever.retrieve(asset) - .tryRecover { - when (it) { - ContentProtectionSchemeRetriever.Error.NotRecognized -> - Try.success(null) - is ContentProtectionSchemeRetriever.Error.Reading -> - Try.failure(it) - } - }.getOrElse { - return Try.failure( - ImportError.Publication(PublicationError(it)) - ) - } - publicationFactory.open( asset, - contentProtectionScheme = drmScheme, allowUserInteraction = false ).onSuccess { publication -> val coverFile = coverStorage.storeCover(publication, coverUrl) .getOrElse { return Try.failure( - ImportError.Publication( - PublicationError.ReadError(ReadError.Access(FileSystemError.IO(it))) - ) + ImportError.FileSystem( + FileSystemError.IO(it)) ) } val id = bookRepository.insertBook( url, - asset.mediaType, - drmScheme, + asset.format, + formatRegistry[asset.format]?.mediaType ?: MediaType.BINARY, publication, coverFile ) diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt index 8e9278649c..d15d9be2fe 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt @@ -7,6 +7,7 @@ package org.readium.r2.testapp.domain import org.readium.r2.lcp.LcpError +import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.file.FileSystemError @@ -20,6 +21,9 @@ sealed class ImportError( override val message: String = "Import failed" + object MissingLcpSupport + : ImportError(DebugError("Lcp support is missing.")) + class LcpAcquisitionFailed( override val cause: LcpError ) : ImportError(cause) @@ -43,6 +47,7 @@ sealed class ImportError( ImportError(cause) fun toUserError(): UserError = when (this) { + is MissingLcpSupport -> UserError(R.string.missing_lcp_support) is Database -> UserError(R.string.import_publication_unable_add_pub_database) is DownloadFailed -> UserError(R.string.import_publication_download_failed) is LcpAcquisitionFailed -> cause.toUserError() diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index e1c1553515..1003b87039 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -6,9 +6,8 @@ package org.readium.r2.testapp.domain -import org.readium.r2.shared.publication.protection.ContentProtectionSchemeRetriever import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.AssetOpener import org.readium.r2.streamer.PublicationFactory import org.readium.r2.testapp.R import org.readium.r2.testapp.utils.UserError @@ -24,12 +23,7 @@ sealed class PublicationError( class UnsupportedScheme(cause: Error) : PublicationError(cause.message, cause.cause) - class UnsupportedContentProtection(cause: Error) : - PublicationError(cause.message, cause.cause) - class UnsupportedArchiveFormat(cause: Error) : - PublicationError(cause.message, cause.cause) - - class UnsupportedPublication(cause: Error) : + class UnsupportedFormat(cause: Error) : PublicationError(cause.message, cause.cause) class InvalidPublication(cause: Error) : @@ -44,11 +38,7 @@ sealed class PublicationError( UserError(R.string.publication_error_invalid_publication) is Unexpected -> UserError(R.string.publication_error_unexpected) - is UnsupportedArchiveFormat -> - UserError(R.string.publication_error_unsupported_archive) - is UnsupportedContentProtection -> - UserError(R.string.publication_error_unsupported_protection) - is UnsupportedPublication -> + is UnsupportedFormat -> UserError(R.string.publication_error_unsupported_asset) is UnsupportedScheme -> UserError(R.string.publication_error_scheme_not_supported) @@ -58,32 +48,22 @@ sealed class PublicationError( companion object { - operator fun invoke(error: AssetRetriever.RetrieveError): PublicationError = + operator fun invoke(error: AssetOpener.OpenError): PublicationError = when (error) { - is AssetRetriever.RetrieveError.Reading -> + is AssetOpener.OpenError.Reading -> ReadError(error.cause) - is AssetRetriever.RetrieveError.FormatNotSupported -> - UnsupportedArchiveFormat(error) - is AssetRetriever.RetrieveError.SchemeNotSupported -> + is AssetOpener.OpenError.FormatNotSupported -> + UnsupportedFormat(error) + is AssetOpener.OpenError.SchemeNotSupported -> UnsupportedScheme(error) } - operator fun invoke(error: ContentProtectionSchemeRetriever.Error): PublicationError = - when (error) { - is ContentProtectionSchemeRetriever.Error.Reading -> - ReadError(error.cause) - ContentProtectionSchemeRetriever.Error.NotRecognized -> - UnsupportedContentProtection(error) - } - operator fun invoke(error: PublicationFactory.OpenError): PublicationError = when (error) { is PublicationFactory.OpenError.Reading -> ReadError(error.cause) is PublicationFactory.OpenError.FormatNotSupported -> - UnsupportedPublication(error) - is PublicationFactory.OpenError.ContentProtectionNotSupported -> - UnsupportedContentProtection(error) + PublicationError(error) } } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index 5afe91eb0e..6f250a62a6 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -6,6 +6,7 @@ package org.readium.r2.testapp.domain +import org.readium.r2.lcp.LcpPublicationRetriever as ReadiumLcpPublicationRetriever import android.content.Context import android.net.Uri import java.io.File @@ -14,7 +15,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import org.readium.r2.lcp.LcpError -import org.readium.r2.lcp.LcpPublicationRetriever as ReadiumLcpPublicationRetriever import org.readium.r2.lcp.LcpService import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.publication.Publication @@ -23,13 +23,14 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.AssetRetriever +import org.readium.r2.shared.util.asset.AssetOpener import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.file.FileSystemError +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.mediatype.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.testapp.data.DownloadRepository import org.readium.r2.testapp.utils.extensions.copyToTempFile @@ -104,7 +105,7 @@ class LocalPublicationRetriever( private val listener: PublicationRetriever.Listener, private val context: Context, private val storageDir: File, - private val assetRetriever: AssetRetriever, + private val assetOpener: AssetOpener, private val formatRegistry: FormatRegistry, createLcpPublicationRetriever: (PublicationRetriever.Listener) -> LcpPublicationRetriever? ) { @@ -150,7 +151,7 @@ class LocalPublicationRetriever( tempFile: File, coverUrl: AbsoluteUrl? = null ) { - val sourceAsset = assetRetriever.retrieve(tempFile) + val sourceAsset = assetOpener.open(tempFile) .getOrElse { listener.onError( ImportError.Publication(PublicationError(it)) @@ -160,23 +161,19 @@ class LocalPublicationRetriever( if ( sourceAsset is ResourceAsset && - sourceAsset.mediaType.matches(MediaType.LCP_LICENSE_DOCUMENT) + sourceAsset.format.conformsTo(Format.LCP_LICENSE_DOCUMENT) ) { if (lcpPublicationRetriever == null) { - listener.onError( - ImportError.Publication( - PublicationError.UnsupportedContentProtection( - DebugError("LCP support is missing.") - ) - ) - ) + listener.onError(ImportError.MissingLcpSupport) } else { lcpPublicationRetriever.retrieve(sourceAsset, tempFile, coverUrl) } return } - val fileExtension = formatRegistry.fileExtension(sourceAsset.mediaType) ?: tempFile.extension + val fileExtension = formatRegistry[sourceAsset.format] + ?.fileExtension?.value + ?: tempFile.extension val fileName = "${UUID.randomUUID()}.$fileExtension" val libraryFile = File(storageDir, fileName) diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt index 24082e781c..18ee44631e 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/OpeningError.kt @@ -25,6 +25,9 @@ sealed class OpeningError( cause: Error ) : OpeningError(cause) + class CannotRender(cause: Error) : + OpeningError(cause) + class AudioEngineInitialization( cause: Error ) : OpeningError(cause) @@ -37,5 +40,7 @@ sealed class OpeningError( cause.toUserError() is RestrictedPublication -> UserError(R.string.publication_error_restricted) + is CannotRender -> + UserError((R.string.opening_publication_cannot_render)) } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt index 9fe369d733..e7989ccd42 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt @@ -72,7 +72,7 @@ class ReaderRepository( val book = checkNotNull(bookRepository.get(bookId)) { "Cannot find book in database." } - val asset = readium.assetRetriever.retrieve( + val asset = readium.assetOpener.open( book.url, book.mediaType ).getOrElse { @@ -85,7 +85,6 @@ class ReaderRepository( val publication = readium.publicationFactory.open( asset, - contentProtectionScheme = book.drmScheme, allowUserInteraction = true ).getOrElse { return Try.failure( @@ -119,11 +118,9 @@ class ReaderRepository( openImage(bookId, publication, initialLocator) else -> Try.failure( - OpeningError.PublicationError( - PublicationError.UnsupportedPublication( + OpeningError.CannotRender( DebugError("No navigator supports this publication.") ) - ) ) } @@ -143,10 +140,8 @@ class ReaderRepository( publication, ExoPlayerEngineProvider(application) ) ?: return Try.failure( - OpeningError.PublicationError( - PublicationError.UnsupportedPublication( - DebugError("Cannot create audio navigator factory.") - ) + OpeningError.CannotRender( + DebugError("Cannot create audio navigator factory.") ) ) @@ -159,7 +154,7 @@ class ReaderRepository( is AudioNavigatorFactory.Error.EngineInitialization -> OpeningError.AudioEngineInitialization(it) is AudioNavigatorFactory.Error.UnsupportedPublication -> - OpeningError.PublicationError(PublicationError.UnsupportedPublication(it)) + OpeningError.CannotRender(it) } ) } diff --git a/test-app/src/main/res/values/strings.xml b/test-app/src/main/res/values/strings.xml index b71298e553..0a23b7231e 100644 --- a/test-app/src/main/res/values/strings.xml +++ b/test-app/src/main/res/values/strings.xml @@ -91,6 +91,7 @@ This will return the publication Return + Cannot render publication. Could not open publication because audio engine initialization failed. Unable to add publication due to an unexpected error on your device @@ -111,13 +112,11 @@ Provided credentials were incorrect You are not allowed to open this publication There is not enough memory on this device to open the publication. - Archive format is not supported. - Content protection is not supported. Publication source is not supported. Publication looks corrupted. An unexpected error occurred. - + This interaction is not available This License has a profile identifier that this app cannot handle, the publication cannot be processed @@ -162,6 +161,7 @@ Unable to decrypt encrypted content key from user key Unable to decrypt encrypted content from content key + The application was compiled without LCP support. Error Could not open publication Import error From 543b7abf627f9a8c2adb07ad82c576cdc2a0ebea Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Sun, 10 Dec 2023 19:25:18 +0100 Subject: [PATCH 72/86] Organisation --- .../readium/r2/lcp/LcpPublicationRetriever.kt | 4 +- .../lcp/license/container/LicenseContainer.kt | 2 +- .../readium/r2/lcp/service/LicensesService.kt | 2 +- .../LcpFallbackContentProtection.kt | 5 +- .../r2/shared/util/asset/AssetOpener.kt | 2 +- .../r2/shared/util/asset/AssetSniffer.kt | 92 ++++++++++--- .../readium/r2/shared/util/data/Container.kt | 1 - .../android/AndroidDownloadManager.kt | 2 +- .../r2/shared/util/format/ContentSniffer.kt | 69 ++++++++++ .../util/{sniff => format}/DefaultSniffers.kt | 100 ++++++-------- .../readium/r2/shared/util/format/Format.kt | 1 - .../util/{sniff => format}/FormatHints.kt | 3 +- .../r2/shared/util/format/FormatRegistry.kt | 5 +- .../shared/util/resource/ResourceFactory.kt | 2 +- .../r2/shared/util/sniff/ContentSniffer.kt | 122 ------------------ .../shared/util/zip/FileZipArchiveProvider.kt | 2 +- .../r2/shared/util/zip/ZipArchiveOpener.kt | 2 +- .../readium/r2/streamer/ParserAssetFactory.kt | 2 +- .../readium/r2/streamer/PublicationFactory.kt | 1 - .../r2/streamer/parser/audio/AudioParser.kt | 2 +- .../parser/epub/EpubPositionsService.kt | 2 +- .../r2/streamer/parser/image/ImageParser.kt | 2 +- .../parser/readium/LcpdfPositionsService.kt | 2 +- .../parser/readium/ReadiumWebPubParser.kt | 1 - .../parser/epub/EpubPositionsServiceTest.kt | 4 +- .../streamer/parser/image/ImageParserTest.kt | 2 +- .../java/org/readium/r2/testapp/Readium.kt | 5 +- .../readium/r2/testapp/domain/Bookshelf.kt | 3 +- .../readium/r2/testapp/domain/ImportError.kt | 4 +- .../r2/testapp/domain/PublicationRetriever.kt | 2 +- .../r2/testapp/reader/ReaderRepository.kt | 4 +- 31 files changed, 219 insertions(+), 233 deletions(-) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/format/ContentSniffer.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/{sniff => format}/DefaultSniffers.kt (93%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{sniff => format}/FormatHints.kt (97%) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/sniff/ContentSniffer.kt diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt index 786c1f6d94..0219564c3e 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt @@ -18,9 +18,9 @@ import org.readium.r2.shared.util.ErrorException import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatHints import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.sniff.FormatHints /** * Utility to acquire a protected publication from an LCP License Document. @@ -28,7 +28,7 @@ import org.readium.r2.shared.util.sniff.FormatHints public class LcpPublicationRetriever( context: Context, private val downloadManager: DownloadManager, - private val assetSniffer: AssetSniffer, + private val assetSniffer: AssetSniffer ) { @JvmInline diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt index a52a019485..b6c33cbefa 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/license/container/LicenseContainer.kt @@ -79,7 +79,7 @@ internal fun createLicenseContainer( format: Format ): LicenseContainer { val licensePath = when { - format.conformsTo(Format.EPUB) -> LICENSE_IN_EPUB + format.conformsTo(Format.EPUB) -> LICENSE_IN_EPUB // Assuming it's a Readium WebPub package (e.g. audiobook, LCPDF, etc.) as a fallback else -> LICENSE_IN_RPF } diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index 90d49c05e0..8a3d9a1555 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -42,9 +42,9 @@ import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatHints import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.sniff.FormatHints import timber.log.Timber internal class LicensesService( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index f7b1459329..45a1c3b2cc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -35,12 +35,13 @@ public class LcpFallbackContentProtection : ContentProtection { !asset.format.conformsTo(Format.RPF_AUDIO_LCP) && !asset.format.conformsTo(Format.RPF_IMAGE_LCP) && !asset.format.conformsTo(Format.RPF_PDF_LCP) - ) { + ) { return Try.failure(ContentProtection.OpenError.AssetNotSupported()) } if (asset !is ContainerAsset) { - return Try.failure(ContentProtection.OpenError.AssetNotSupported() + return Try.failure( + ContentProtection.OpenError.AssetNotSupported() ) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt index b33ace9bc6..25c8ebb2bf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt @@ -12,11 +12,11 @@ import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatHints import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceFactory -import org.readium.r2.shared.util.sniff.FormatHints import org.readium.r2.shared.util.toUrl import timber.log.Timber diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt index c583398990..e345ab5aaf 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt @@ -13,19 +13,61 @@ import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.file.FileResource +import org.readium.r2.shared.util.format.ArchiveSniffer +import org.readium.r2.shared.util.format.BitmapSniffer +import org.readium.r2.shared.util.format.BlobSniffer +import org.readium.r2.shared.util.format.ContainerSniffer +import org.readium.r2.shared.util.format.ContentSniffer +import org.readium.r2.shared.util.format.EpubSniffer import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatHints +import org.readium.r2.shared.util.format.FormatHintsSniffer +import org.readium.r2.shared.util.format.HtmlSniffer +import org.readium.r2.shared.util.format.JsonSniffer +import org.readium.r2.shared.util.format.LcpLicenseSniffer +import org.readium.r2.shared.util.format.LcpSniffer +import org.readium.r2.shared.util.format.LpfSniffer +import org.readium.r2.shared.util.format.OpdsSniffer +import org.readium.r2.shared.util.format.PdfSniffer +import org.readium.r2.shared.util.format.RarSniffer +import org.readium.r2.shared.util.format.RpfSniffer +import org.readium.r2.shared.util.format.RwpmSniffer +import org.readium.r2.shared.util.format.W3cWpubSniffer +import org.readium.r2.shared.util.format.XhtmlSniffer +import org.readium.r2.shared.util.format.ZipSniffer import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.borrow -import org.readium.r2.shared.util.sniff.ContentSniffer -import org.readium.r2.shared.util.sniff.FormatHints import org.readium.r2.shared.util.tryRecover import org.readium.r2.shared.util.use +import org.readium.r2.shared.util.zip.ZipArchiveOpener public class AssetSniffer( - private val contentSniffer: ContentSniffer, - private val archiveOpener: ArchiveOpener + private val contentSniffers: List = defaultContentSniffers, + private val archiveOpener: ArchiveOpener = ZipArchiveOpener() ) { + + public companion object { + + public val defaultContentSniffers: List = listOf( + ZipSniffer, + RarSniffer, + EpubSniffer, + LpfSniffer, + ArchiveSniffer, + RpfSniffer, + PdfSniffer, + XhtmlSniffer, + HtmlSniffer, + BitmapSniffer, + JsonSniffer, + OpdsSniffer, + LcpLicenseSniffer, + LcpSniffer, + W3cWpubSniffer, + RwpmSniffer + ) + } public suspend fun sniffOpen( source: Resource, hints: FormatHints = FormatHints() @@ -53,23 +95,43 @@ public class AssetSniffer( private suspend fun sniff( format: Format?, source: Either>, - hints: FormatHints + hints: FormatHints, + excludeHintsSniffer: FormatHintsSniffer? = null, + excludeBlobSniffer: BlobSniffer? = null, + excludeContainerSniffer: ContainerSniffer? = null ): Try { - contentSniffer.sniffHints(format, hints) - ?.takeIf { format == null || it.conformsTo(format) } - ?.takeIf { it != format} - ?.let { return sniff(it, source, hints) } + for (sniffer in contentSniffers) { + sniffer + .takeIf { it != excludeHintsSniffer } + ?.sniffHints(format, hints) + ?.takeIf { format == null || it.conformsTo(format) } + ?.takeIf { it != format } + ?.let { return sniff(it, source, hints, excludeHintsSniffer = sniffer) } + } when (source) { is Either.Left -> - contentSniffer.sniffBlob(format, source.value) + for (sniffer in contentSniffers) { + sniffer + .takeIf { it != excludeBlobSniffer } + ?.sniffBlob(format, source.value) + ?.getOrElse { return Try.failure(SniffError.Reading(it)) } + ?.takeIf { format == null || it.conformsTo(format) } + ?.takeIf { it != format } + ?.let { return sniff(it, source, hints, excludeBlobSniffer = sniffer) } + } + is Either.Right -> - contentSniffer.sniffContainer(format, source.value) + for (sniffer in contentSniffers) { + sniffer + .takeIf { it != excludeContainerSniffer } + ?.sniffContainer(format, source.value) + ?.getOrElse { return Try.failure(SniffError.Reading(it)) } + ?.takeIf { format == null || it.conformsTo(format) } + ?.takeIf { it != format } + ?.let { return sniff(it, source, hints, excludeContainerSniffer = sniffer) } + } } - .getOrElse { return Try.failure(SniffError.Reading(it)) } - ?.takeIf { format == null || it.conformsTo(format) } - ?.takeIf { it != format } - ?.let { return sniff(it, source, hints) } if (source is Either.Left) { tryOpenArchive(format, source.value) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt index dce2eedbda..bf81816be4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Container.kt @@ -11,7 +11,6 @@ import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.use /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index 04c9f42fd0..7f6d56349b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -242,7 +242,7 @@ public class AndroidDownloadManager internal constructor( SystemDownloadManager.STATUS_SUCCESSFUL -> { prepareResult( Uri.parse(facade.localUri!!)!!.toFile(), - mediaType= facade.mediaType?.let { MediaType(it) } + mediaType = facade.mediaType?.let { MediaType(it) } ) .onSuccess { download -> listenersForId.forEach { it.onDownloadCompleted(id, download) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/ContentSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/ContentSniffer.kt new file mode 100644 index 0000000000..91b24ac96d --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/ContentSniffer.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.format + +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.Readable + +/** + * Tries to refine a [Format] from media type and file extension hints. + */ +public interface FormatHintsSniffer { + + public fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? +} + +/** + * Tries to refine a [Format] by sniffing a [Readable] blob. + */ +public interface BlobSniffer { + + public suspend fun sniffBlob( + format: Format?, + source: Readable + ): Try +} + +/** + * Tries to Refine a [Format] by sniffing a [Container]. + */ +public interface ContainerSniffer { + + public suspend fun sniffContainer( + format: Format?, + container: Container + ): Try +} + +public interface ContentSniffer : + FormatHintsSniffer, + BlobSniffer, + ContainerSniffer { + + public override fun sniffHints( + format: Format?, + hints: FormatHints + ): Format? = + null + + public override suspend fun sniffBlob( + format: Format?, + source: Readable + ): Try = + Try.success(format) + + public override suspend fun sniffContainer( + format: Format?, + container: Container + ): Try = + Try.success(format) +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/DefaultSniffers.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt similarity index 93% rename from readium/shared/src/main/java/org/readium/r2/shared/util/sniff/DefaultSniffers.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt index 054cbb72d3..232c27df23 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/DefaultSniffers.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.sniff +package org.readium.r2.shared.util.format import java.util.Locale import org.json.JSONObject @@ -25,32 +25,9 @@ import org.readium.r2.shared.util.data.decodeRwpm import org.readium.r2.shared.util.data.decodeString import org.readium.r2.shared.util.data.decodeXml import org.readium.r2.shared.util.data.readDecodeOrElse -import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.getOrDefault import org.readium.r2.shared.util.getOrElse -public object DefaultContentSniffer - : ContentSniffer by CompositeContentSniffer( - listOf( - RpfSniffer, - EpubSniffer, - LpfSniffer, - ArchiveSniffer, - PdfSniffer, - BitmapSniffer, - XhtmlSniffer, - HtmlSniffer, - OpdsSniffer, - LcpLicenseSniffer, - LcpSniffer, - W3cWpubSniffer, - RwpmSniffer, - JsonSniffer, - ZipSniffer, - RarSniffer - ) -) - /** * Sniffs an XHTML document. * @@ -76,7 +53,7 @@ public object XhtmlSniffer : ContentSniffer { source: Readable ): Try { if (format?.conformsTo(Format.XML) == false || !source.canReadWholeBlob()) { - return Try.success(format) + return Try.success(format) } source.readDecodeOrElse( @@ -192,7 +169,7 @@ public object OpdsSniffer : ContentSniffer { format: Format?, source: Readable ): Try { - if (!source.canReadWholeBlob() ) { + if (!source.canReadWholeBlob()) { return Try.success(format) } @@ -253,10 +230,10 @@ public object OpdsSniffer : ContentSniffer { firstOrNull { it.rels.any(predicate) } if (rwpm.links.firstWithRelMatching { - it.startsWith( + it.startsWith( "http://opds-spec.org/acquisition" ) - } != null + } != null ) { return Try.success(Format.OPDS2_PUBLICATION) } @@ -295,8 +272,8 @@ public object LcpLicenseSniffer : ContentSniffer { if ( format?.conformsTo(Format.JSON) == false || !source.canReadWholeBlob() - ) { - return Try.success(format) + ) { + return Try.success(format) } source.containsJsonKeys("id", "issued", "provider", "encryption") @@ -394,8 +371,8 @@ public object RwpmSniffer : ContentSniffer { if ( format?.conformsTo(Format.JSON) == false || !source.canReadWholeBlob() - ) { - return Try.success(format) + ) { + return Try.success(format) } val manifest: Manifest = @@ -826,7 +803,8 @@ public object JsonSniffer : ContentSniffer { hints: FormatHints ): Format? { if (hints.hasFileExtension("json") || - hints.hasMediaType("application/json")) { + hints.hasMediaType("application/json") + ) { return Format.JSON } @@ -900,34 +878,34 @@ public object LcpSniffer : ContentSniffer { format: Format?, container: Container ): Try { - when { - format?.conformsTo(Format.RPF) == true -> { - val isLcpProtected = RelativeUrl("license.lcpl")!! in container || - hasLcpSchemeInManifest(container) - .getOrElse { return Try.failure(it) } - - if (isLcpProtected) { - val newFormat = when (format) { - Format.RPF_IMAGE -> Format.RPF_IMAGE_LCP - Format.RPF_AUDIO -> Format.RPF_AUDIO_LCP - Format.RPF_PDF -> Format.RPF_PDF_LCP - Format.RPF -> Format.RPF_LCP - else -> null - } - newFormat?.let { return Try.success(it) } - } - } - - format?.conformsTo(Format.EPUB) == true -> { - val isLcpProtected = RelativeUrl("META-INF/license.lcpl")!! in container || - hasLcpSchemeInEncryptionXml(container) - .getOrElse { return Try.failure(it) } - - if (isLcpProtected) { - return Try.success(Format.EPUB_LCP) - } - } - } + when { + format?.conformsTo(Format.RPF) == true -> { + val isLcpProtected = RelativeUrl("license.lcpl")!! in container || + hasLcpSchemeInManifest(container) + .getOrElse { return Try.failure(it) } + + if (isLcpProtected) { + val newFormat = when (format) { + Format.RPF_IMAGE -> Format.RPF_IMAGE_LCP + Format.RPF_AUDIO -> Format.RPF_AUDIO_LCP + Format.RPF_PDF -> Format.RPF_PDF_LCP + Format.RPF -> Format.RPF_LCP + else -> null + } + newFormat?.let { return Try.success(it) } + } + } + + format?.conformsTo(Format.EPUB) == true -> { + val isLcpProtected = RelativeUrl("META-INF/license.lcpl")!! in container || + hasLcpSchemeInEncryptionXml(container) + .getOrElse { return Try.failure(it) } + + if (isLcpProtected) { + return Try.success(Format.EPUB_LCP) + } + } + } return Try.success(format) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt index 4e59635b67..a022982215 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt @@ -60,7 +60,6 @@ public value class Format(public val id: String) { public val TIFF: Format = Format("tiff") public val WEBP: Format = Format("webp") - public val XML: Format = Format("xml") public val XHTML: Format = Format("xml.html") public val ATOM: Format = Format("xml.atom") diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/FormatHints.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt similarity index 97% rename from readium/shared/src/main/java/org/readium/r2/shared/util/sniff/FormatHints.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt index 82a8457b12..a41eb626b2 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/FormatHints.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt @@ -4,10 +4,9 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.util.sniff +package org.readium.r2.shared.util.format import java.nio.charset.Charset -import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.mediatype.MediaType /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt index 420269f0b7..e4ace1f53a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt @@ -27,7 +27,10 @@ public class FormatRegistry( Format.RPF_IMAGE to FormatInfo(MediaType.DIVINA, FileExtension("divina")), Format.RWPM_IMAGE to FormatInfo(MediaType.DIVINA_MANIFEST, FileExtension("json")), Format.EPUB to FormatInfo(MediaType.EPUB, FileExtension("epub")), - Format.LCP_LICENSE_DOCUMENT to FormatInfo(MediaType.LCP_LICENSE_DOCUMENT, FileExtension("lcpl")), + Format.LCP_LICENSE_DOCUMENT to FormatInfo( + MediaType.LCP_LICENSE_DOCUMENT, + FileExtension("lcpl") + ), Format.RPF_AUDIO_LCP to FormatInfo(MediaType.LCP_PROTECTED_AUDIOBOOK, FileExtension("lcpa")), Format.RPF_PDF_LCP to FormatInfo(MediaType.LCP_PROTECTED_PDF, FileExtension("lcpdf")), Format.PDF to FormatInfo(MediaType.PDF, FileExtension("pdf")), diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt index d981c68beb..0d222fb9b6 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/resource/ResourceFactory.kt @@ -47,7 +47,7 @@ public class CompositeResourceFactory( public constructor(vararg factories: ResourceFactory) : this(factories.toList()) override suspend fun create( - url: AbsoluteUrl, + url: AbsoluteUrl ): Try { for (factory in factories) { factory.create(url) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/ContentSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/ContentSniffer.kt deleted file mode 100644 index 0e7d8c7b38..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/sniff/ContentSniffer.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.util.sniff - -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.Readable -import org.readium.r2.shared.util.format.Format -import org.readium.r2.shared.util.getOrElse -import timber.log.Timber - -/** - * Tries to refine a [Format] from media type and file extension hints. - */ -public interface FormatHintsSniffer { - - public fun sniffHints( - format: Format?, - hints: FormatHints - ): Format? -} - -/** - * Tries to refine a [Format] by sniffing a [Readable] blob. - */ -public interface BlobSniffer { - - public suspend fun sniffBlob( - format: Format?, - source: Readable - ): Try -} - -/** - * Tries to Refine a [Format] by sniffing a [Container]. - */ -public interface ContainerSniffer { - - public suspend fun sniffContainer( - format: Format?, - container: Container - ): Try -} - -public interface ContentSniffer : - FormatHintsSniffer, - BlobSniffer, - ContainerSniffer { - - public override fun sniffHints( - format: Format?, - hints: FormatHints - ): Format? = - null - - public override suspend fun sniffBlob( - format: Format?, - source: Readable - ): Try = - Try.success(format) - - - public override suspend fun sniffContainer( - format: Format?, - container: Container - ): Try = - Try.success(format) -} - -public class CompositeContentSniffer( - private val sniffers: List -) : ContentSniffer { - - override fun sniffHints( - format: Format?, - hints: FormatHints - ): Format? { - for (sniffer in sniffers) { - Timber.d("Trying hints ${sniffer.javaClass.simpleName}") - sniffer.sniffHints(format, hints) - .takeIf { it != format } - ?.let { return it } - } - - return format - } - - override suspend fun sniffBlob( - format: Format?, - source: Readable - ): Try { - for (sniffer in sniffers) { - Timber.d("Trying blob ${sniffer.javaClass.simpleName}") - sniffer.sniffBlob(format, source) - .getOrElse { return Try.failure(it) } - .takeIf { it != format } - ?.let { return Try.success(it) } - } - - return Try.success(format) - } - - override suspend fun sniffContainer( - format: Format?, - container: Container - ): Try { - for (sniffer in sniffers) { - Timber.d("Trying container ${sniffer.javaClass.simpleName}") - sniffer.sniffContainer(format, container) - .getOrElse { return Try.failure(it) } - .takeIf { it != format } - ?.let { return Try.success(it) } - } - - return Try.success(format) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index 9a74b69d11..4e799fc3f0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -16,13 +16,13 @@ import kotlinx.coroutines.withContext import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.ArchiveOpener import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.asset.SniffError /** * An [ArchiveOpener] to open local ZIP files with Java's [ZipFile]. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt index 2786e76063..9b0003c93a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt @@ -9,11 +9,11 @@ package org.readium.r2.shared.util.zip import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.ArchiveOpener import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.resource.Resource -import org.readium.r2.shared.util.asset.SniffError public class ZipArchiveOpener : ArchiveOpener { diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt index afb4c4e03a..f0cbe1a0cc 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt @@ -20,9 +20,9 @@ import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.decodeRwpm import org.readium.r2.shared.util.data.readDecodeOrElse import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpContainer -import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.streamer.parser.PublicationParser import timber.log.Timber diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 0578cc854c..a4d8ad4d76 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -171,7 +171,6 @@ public class PublicationFactory( val parserAsset = PublicationParser.Asset(asset.format, asset.container) - return openParserAsset(parserAsset, compositeOnCreatePublication, warnings) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index 0729e19dd9..2a7f1901ff 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -15,12 +15,12 @@ import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.AssetSniffer +import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger -import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt index af2a7af69c..2135cfb045 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubPositionsService.kt @@ -17,10 +17,10 @@ import org.readium.r2.shared.publication.presentation.Presentation import org.readium.r2.shared.publication.presentation.presentation import org.readium.r2.shared.publication.services.PositionsService import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.use /** diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index 84ded4f5d2..7e84e3f1a6 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -16,13 +16,13 @@ import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.AssetSniffer +import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt index 61028847fb..72f773540e 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/LcpdfPositionsService.kt @@ -109,7 +109,7 @@ internal class LcpdfPositionsService( } } - companion object { + companion object { fun create(pdfFactory: PdfDocumentFactory<*>): (Publication.Service.Context) -> LcpdfPositionsService = { serviceContext -> LcpdfPositionsService( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index 20f84e73c5..8f3c384620 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -97,4 +97,3 @@ public class ReadiumWebPubParser( return Try.success(publicationBuilder) } } - diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt index b039879d43..48f28c7591 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubPositionsServiceTest.kt @@ -21,12 +21,12 @@ import org.readium.r2.shared.publication.presentation.Presentation 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.resource.ArchiveProperties -import org.readium.r2.shared.util.resource.archive import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadTry import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.ArchiveProperties import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.archive import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index 4c386a77fb..4d8b9f16e5 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -22,11 +22,11 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.file.FileResource +import org.readium.r2.shared.util.format.DefaultContentSniffer import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.SingleResourceContainer -import org.readium.r2.shared.util.sniff.DefaultContentSniffer import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.zip.ZipArchiveOpener import org.readium.r2.streamer.parseBlocking diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index c5b54cd8e2..97d0a0127f 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -25,7 +25,6 @@ import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpResourceFactory import org.readium.r2.shared.util.resource.CompositeResourceFactory -import org.readium.r2.shared.util.sniff.DefaultContentSniffer import org.readium.r2.shared.util.zip.ZipArchiveOpener import org.readium.r2.streamer.PublicationFactory @@ -41,7 +40,7 @@ class Readium(context: Context) { FormatRegistry() private val assetSniffer = - AssetSniffer(DefaultContentSniffer, archiveOpener) + AssetSniffer(archiveOpener = archiveOpener) val httpClient = DefaultHttpClient() @@ -54,7 +53,7 @@ class Readium(context: Context) { val assetOpener = AssetOpener( assetSniffer, resourceFactory, - archiveOpener, + archiveOpener ) val downloadManager = AndroidDownloadManager( diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index cecfef70aa..031c60202e 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -141,7 +141,8 @@ class Bookshelf( .getOrElse { return Try.failure( ImportError.FileSystem( - FileSystemError.IO(it)) + FileSystemError.IO(it) + ) ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt index d15d9be2fe..21d0f2af76 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt @@ -21,8 +21,8 @@ sealed class ImportError( override val message: String = "Import failed" - object MissingLcpSupport - : ImportError(DebugError("Lcp support is missing.")) + object MissingLcpSupport : + ImportError(DebugError("Lcp support is missing.")) class LcpAcquisitionFailed( override val cause: LcpError diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt index 6f250a62a6..449cd36065 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt @@ -6,7 +6,6 @@ package org.readium.r2.testapp.domain -import org.readium.r2.lcp.LcpPublicationRetriever as ReadiumLcpPublicationRetriever import android.content.Context import android.net.Uri import java.io.File @@ -15,6 +14,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import org.readium.r2.lcp.LcpError +import org.readium.r2.lcp.LcpPublicationRetriever as ReadiumLcpPublicationRetriever import org.readium.r2.lcp.LcpService import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.publication.Publication diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt index e7989ccd42..9b0ae42868 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt @@ -119,8 +119,8 @@ class ReaderRepository( else -> Try.failure( OpeningError.CannotRender( - DebugError("No navigator supports this publication.") - ) + DebugError("No navigator supports this publication.") + ) ) } From d924a0589a5e643393c4c261c0812abd30bfa155 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 11 Dec 2023 10:44:21 +0100 Subject: [PATCH 73/86] Fail on reading errors in Adept and fallback Lcp sniffing --- .../org/readium/r2/shared/util/format/DefaultSniffers.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt index 232c27df23..6135f3ffaa 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt @@ -849,7 +849,7 @@ public object AdeptSniffer : ContentSniffer { container[Url("META-INF/encryption.xml")!!] ?.readDecodeOrElse( decode = { it.decodeXml() }, - recover = { null } + recover = { return Try.failure(it) } ) ?.get("EncryptedData", EpubEncryption.ENC) ?.flatMap { it.get("KeyInfo", EpubEncryption.SIG) } @@ -914,8 +914,7 @@ public object LcpSniffer : ContentSniffer { val manifest = container[Url("manifest.json")!!] ?.readDecodeOrElse( decode = { it.decodeRwpm() }, - recoverRead = { return Try.success(false) }, - recoverDecode = { return Try.success(false) } + recover = { return Try.failure(it) } ) ?: return Try.success(false) val manifestHasLcpScheme = manifest From fa605cbc0734c2807c3ae5f1cf47b01c069b27a2 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 11 Dec 2023 17:12:05 +0100 Subject: [PATCH 74/86] Fix tests --- .../r2/shared/util/asset/AssetSniffer.kt | 16 +- .../r2/shared/util/format/DefaultSniffers.kt | 53 +- .../r2/shared/util/format/FormatHints.kt | 13 +- .../AdeptFallbackContentProtectionTest.kt | 85 --- .../LcpFallbackContentProtectionTest.kt | 102 ---- .../shared/util/format/DefaultSniffersTest.kt | 134 ++++ .../format}/TestContainer.kt | 8 +- .../shared/util/mediatype/AssetSnifferTest.kt | 570 ++++++++++++++++++ .../util/mediatype/FormatRegistryTest.kt | 27 +- .../util/mediatype/MediaTypeRetrieverTest.kt | 537 ----------------- .../shared/util/{mediatype => asset}/any.json | 0 .../audiobook-lcp.unknown | Bin .../audiobook-package.unknown | Bin .../audiobook-wrongtype.json | 0 .../util/{mediatype => asset}/audiobook.json | 0 .../util/{mediatype => asset}/cbz.unknown | Bin .../divina-package.unknown | Bin .../util/{mediatype => asset}/divina.json | 0 .../util/{mediatype => asset}/epub.unknown | Bin .../html-doctype-case.unknown | 0 .../util/{mediatype => asset}/html.unknown | 0 .../util/{mediatype => asset}/lcpl.unknown | 0 .../lpf-index-html.unknown | Bin .../util/{mediatype => asset}/lpf.unknown | Bin .../opds-authentication.json | 0 .../{mediatype => asset}/opds1-entry.unknown | 0 .../{mediatype => asset}/opds1-feed.unknown | 0 .../util/{mediatype => asset}/opds2-feed.json | 0 .../opds2-publication.json | 2 +- .../util/{mediatype => asset}/pdf-lcp.unknown | Bin .../util/{mediatype => asset}/pdf.unknown | Bin .../util/{mediatype => asset}/png.unknown | Bin .../shared/util/{mediatype => asset}/unknown | 0 .../util/{mediatype => asset}/unknown.zip | Bin .../util/{mediatype => asset}/w3c-wpub.json | 0 .../webpub-package.unknown | Bin .../util/{mediatype => asset}/webpub.json | 0 .../util/{mediatype => asset}/xhtml.unknown | 0 .../util/{mediatype => asset}/zab.unknown | Bin .../streamer/parser/image/ImageParserTest.kt | 3 +- 40 files changed, 783 insertions(+), 767 deletions(-) delete mode 100644 readium/shared/src/test/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtectionTest.kt delete mode 100644 readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt create mode 100644 readium/shared/src/test/java/org/readium/r2/shared/util/format/DefaultSniffersTest.kt rename readium/shared/src/test/java/org/readium/r2/shared/{publication/protection => util/format}/TestContainer.kt (78%) create mode 100644 readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt delete mode 100644 readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/any.json (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/audiobook-lcp.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/audiobook-package.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/audiobook-wrongtype.json (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/audiobook.json (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/cbz.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/divina-package.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/divina.json (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/epub.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/html-doctype-case.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/html.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/lcpl.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/lpf-index-html.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/lpf.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/opds-authentication.json (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/opds1-entry.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/opds1-feed.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/opds2-feed.json (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/opds2-publication.json (94%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/pdf-lcp.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/pdf.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/png.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/unknown.zip (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/w3c-wpub.json (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/webpub-package.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/webpub.json (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/xhtml.unknown (100%) rename readium/shared/src/test/resources/org/readium/r2/shared/util/{mediatype => asset}/zab.unknown (100%) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt index e345ab5aaf..78a14d76b8 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt @@ -13,6 +13,7 @@ import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.file.FileResource +import org.readium.r2.shared.util.format.AdeptSniffer import org.readium.r2.shared.util.format.ArchiveSniffer import org.readium.r2.shared.util.format.BitmapSniffer import org.readium.r2.shared.util.format.BlobSniffer @@ -41,6 +42,7 @@ import org.readium.r2.shared.util.resource.borrow import org.readium.r2.shared.util.tryRecover import org.readium.r2.shared.util.use import org.readium.r2.shared.util.zip.ZipArchiveOpener +import timber.log.Timber public class AssetSniffer( private val contentSniffers: List = defaultContentSniffers, @@ -64,6 +66,7 @@ public class AssetSniffer( OpdsSniffer, LcpLicenseSniffer, LcpSniffer, + AdeptSniffer, W3cWpubSniffer, RwpmSniffer ) @@ -74,7 +77,10 @@ public class AssetSniffer( ): Try = sniff(null, Either.Left(source), hints) - public suspend fun sniffOpen(file: File, hints: FormatHints): Try = + public suspend fun sniffOpen( + file: File, + hints: FormatHints = FormatHints() + ): Try = sniff(null, Either.Left(FileResource(file)), hints) public suspend fun sniff( @@ -83,7 +89,10 @@ public class AssetSniffer( ): Try = sniffOpen(source.borrow(), hints).map { it.format } - public suspend fun sniff(file: File, hints: FormatHints): Try = + public suspend fun sniff( + file: File, + hints: FormatHints = FormatHints() + ): Try = FileResource(file).use { sniff(it, hints) } public suspend fun sniff( @@ -103,6 +112,7 @@ public class AssetSniffer( for (sniffer in contentSniffers) { sniffer .takeIf { it != excludeHintsSniffer } + .also { Timber.d("Trying $sniffer hints") } ?.sniffHints(format, hints) ?.takeIf { format == null || it.conformsTo(format) } ?.takeIf { it != format } @@ -114,6 +124,7 @@ public class AssetSniffer( for (sniffer in contentSniffers) { sniffer .takeIf { it != excludeBlobSniffer } + .also { Timber.d("Trying $sniffer blob") } ?.sniffBlob(format, source.value) ?.getOrElse { return Try.failure(SniffError.Reading(it)) } ?.takeIf { format == null || it.conformsTo(format) } @@ -125,6 +136,7 @@ public class AssetSniffer( for (sniffer in contentSniffers) { sniffer .takeIf { it != excludeContainerSniffer } + .also { Timber.d("Trying $sniffer container") } ?.sniffContainer(format, source.value) ?.getOrElse { return Try.failure(SniffError.Reading(it)) } ?.takeIf { format == null || it.conformsTo(format) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt index 6135f3ffaa..4b51e8ff94 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt @@ -25,6 +25,7 @@ import org.readium.r2.shared.util.data.decodeRwpm import org.readium.r2.shared.util.data.decodeString import org.readium.r2.shared.util.data.decodeXml import org.readium.r2.shared.util.data.readDecodeOrElse +import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.getOrDefault import org.readium.r2.shared.util.getOrElse @@ -52,7 +53,7 @@ public object XhtmlSniffer : ContentSniffer { format: Format?, source: Readable ): Try { - if (format?.conformsTo(Format.XML) == false || !source.canReadWholeBlob()) { + if (format != null && format != Format.XML || !source.canReadWholeBlob()) { return Try.success(format) } @@ -91,7 +92,7 @@ public object HtmlSniffer : ContentSniffer { format: Format?, source: Readable ): Try { - if (!source.canReadWholeBlob()) { + if (format != null || !source.canReadWholeBlob()) { return Try.success(format) } @@ -174,10 +175,7 @@ public object OpdsSniffer : ContentSniffer { } sniffBlobXml(format, source) - .getOrElse { return Try.failure(it) } - ?.let { return Try.success(it) } - - sniffBlobJson(format, source) + .flatMap { sniffBlobJson(it, source) } .getOrElse { return Try.failure(it) } ?.let { return Try.success(it) } @@ -185,7 +183,7 @@ public object OpdsSniffer : ContentSniffer { } private suspend fun sniffBlobXml(format: Format?, source: Readable): Try { - if (format?.conformsTo(Format.XML) == false) { + if (format != null && format != Format.XML) { return Try.success(format) } @@ -207,7 +205,7 @@ public object OpdsSniffer : ContentSniffer { } private suspend fun sniffBlobJson(format: Format?, source: Readable): Try { - if (format?.conformsTo(Format.JSON) == false) { + if (format !in listOf(null, Format.JSON, Format.RWPM)) { return Try.success(format) } @@ -270,7 +268,7 @@ public object LcpLicenseSniffer : ContentSniffer { source: Readable ): Try { if ( - format?.conformsTo(Format.JSON) == false || + format != null && format != Format.JSON || !source.canReadWholeBlob() ) { return Try.success(format) @@ -369,7 +367,7 @@ public object RwpmSniffer : ContentSniffer { source: Readable ): Try { if ( - format?.conformsTo(Format.JSON) == false || + format != null && format != Format.JSON || !source.canReadWholeBlob() ) { return Try.success(format) @@ -399,6 +397,7 @@ public object RwpmSniffer : ContentSniffer { /** Sniffs a Readium Web Publication, protected or not by LCP. */ public object RpfSniffer : ContentSniffer { + override fun sniffHints( format: Format?, hints: FormatHints @@ -445,7 +444,14 @@ public object RpfSniffer : ContentSniffer { container: Container ): Try { // Recognize exploded RPF. - if (format?.conformsTo(Format.ZIP) == false) { + if ( + format != null && format != Format.ZIP && format != Format.RPF || + format in listOf( + Format.RPF_AUDIO, + Format.RPF_IMAGE, + Format.RPF_PDF + ) + ) { return Try.success(format) } @@ -466,11 +472,8 @@ public object RpfSniffer : ContentSniffer { if (manifest.conformsTo(Publication.Profile.PDF)) { return Try.success(Format.RPF_PDF) } - if (manifest.linkWithRel("self")?.mediaType?.matches("application/webpub+json") == true) { - Try.success(Format.RPF) - } - return Try.success(format) + return Try.success(Format.RPF) } } @@ -481,7 +484,7 @@ public object W3cWpubSniffer : ContentSniffer { format: Format?, source: Readable ): Try { - if (!source.canReadWholeBlob() || format?.conformsTo(Format.JSON) == false) { + if (format != null && format != Format.JSON || !source.canReadWholeBlob()) { return Try.success(format) } @@ -527,7 +530,7 @@ public object EpubSniffer : ContentSniffer { container: Container ): Try { // Recognize exploded EPUBs. - if (format?.conformsTo(Format.ZIP) == false) { + if (format != null && format != Format.ZIP || format == Format.EPUB) { return Try.success(format) } @@ -573,7 +576,7 @@ public object LpfSniffer : ContentSniffer { container: Container ): Try { // Recognize exploded LPFs. - if (format?.conformsTo(Format.ZIP) == false) { + if (format != null && format != Format.ZIP || format == Format.LPF) { return Try.success(format) } @@ -737,6 +740,10 @@ public object ArchiveSniffer : ContentSniffer { } == true } + if (container.entries.isEmpty()) { + return Try.success(format) + } + if ( archiveContainsOnlyExtensions(cbzExtensions) && format?.conformsTo(Format.ZIP) != false // Recognize exploded CBZ/CBR @@ -786,6 +793,10 @@ public object PdfSniffer : ContentSniffer { format: Format?, source: Readable ): Try { + if (format != null) { + return Try.success(format) + } + source.read(0L until 5L) .getOrElse { return Try.failure(it) } .let { tryOrNull { it.toString(Charsets.UTF_8) } } @@ -819,7 +830,7 @@ public object JsonSniffer : ContentSniffer { format: Format?, source: Readable ): Try { - if (!source.canReadWholeBlob()) { + if (format != null || !source.canReadWholeBlob()) { return Try.success(format) } @@ -842,7 +853,7 @@ public object AdeptSniffer : ContentSniffer { format: Format?, container: Container ): Try { - if (format?.conformsTo(Format.EPUB) != true) { + if (format != Format.EPUB) { return Try.success(format) } @@ -896,7 +907,7 @@ public object LcpSniffer : ContentSniffer { } } - format?.conformsTo(Format.EPUB) == true -> { + format == Format.EPUB -> { val isLcpProtected = RelativeUrl("META-INF/license.lcpl")!! in container || hasLcpSchemeInEncryptionXml(container) .getOrElse { return Try.failure(it) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt index a41eb626b2..368672cdd4 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt @@ -15,10 +15,13 @@ import org.readium.r2.shared.util.mediatype.MediaType public data class FormatHints( val format: Format? = null, val mediaTypes: List = emptyList(), - val fileExtensions: List = emptyList() + val fileExtensions: List = emptyList() ) { public companion object { - public operator fun invoke(mediaType: MediaType? = null, fileExtension: String? = null): FormatHints = + public operator fun invoke( + mediaType: MediaType? = null, + fileExtension: FileExtension? = null + ): FormatHints = FormatHints( mediaTypes = listOfNotNull(mediaType), fileExtensions = listOfNotNull(fileExtension) @@ -30,7 +33,7 @@ public data class FormatHints( ): FormatHints = FormatHints( mediaTypes = mediaTypes.mapNotNull { MediaType(it) }, - fileExtensions = fileExtensions + fileExtensions = fileExtensions.map { FileExtension(it) } ) } @@ -45,7 +48,7 @@ public data class FormatHints( */ public fun addFileExtension(fileExtension: String?): FormatHints { fileExtension ?: return this - return copy(fileExtensions = fileExtensions + fileExtension) + return copy(fileExtensions = fileExtensions + FileExtension(fileExtension)) } /** Finds the first [Charset] declared in the media types' `charset` parameter. */ @@ -54,7 +57,7 @@ public data class FormatHints( /** Returns whether this context has any of the given file extensions, ignoring case. */ public fun hasFileExtension(vararg fileExtensions: String): Boolean { - val fileExtensionsHints = this.fileExtensions.map { it.lowercase() } + val fileExtensionsHints = this.fileExtensions.map { it.value.lowercase() } for (fileExtension in fileExtensions.map { it.lowercase() }) { if (fileExtensionsHints.contains(fileExtension)) { return true diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtectionTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtectionTest.kt deleted file mode 100644 index be8f74fdc3..0000000000 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtectionTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.publication.protection - -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test -import org.junit.runner.RunWith -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.asset.ContainerAsset -import org.readium.r2.shared.util.checkSuccess -import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.mediatype.MediaType -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class AdeptFallbackContentProtectionTest { - - @Test - fun `Sniff no content protection`() { - assertFalse(supports(mediaType = MediaType.EPUB, resources = emptyMap()).checkSuccess()) - } - - @Test - fun `Sniff EPUB with empty encryption xml`() { - assertFalse( - supports( - mediaType = MediaType.EPUB, - resources = mapOf( - "META-INF/encryption.xml" to """""" - ) - ).checkSuccess() - ) - } - - @Test - fun `Sniff Adobe ADEPT`() { - assertTrue( - supports( - mediaType = MediaType.EPUB, - resources = mapOf( - "META-INF/encryption.xml" to """ - - - - urn:uuid:2c43729c-b985-4531-8e86-ae75ce5e5da9 - - - - - - """ - ) - ).checkSuccess() - ) - } - - @Test - fun `Sniff Adobe ADEPT from rights xml`() { - assertTrue( - supports( - mediaType = MediaType.EPUB, - resources = mapOf( - "META-INF/encryption.xml" to """""", - "META-INF/rights.xml" to """""" - ) - ).checkSuccess() - ) - } - - private fun supports(mediaType: MediaType, resources: Map): Try = runBlocking { - AdeptFallbackContentProtection().supports( - ContainerAsset( - mediaType = mediaType, - container = TestContainer(resources.mapKeys { Url(it.key)!! }) - ) - ) - } -} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt deleted file mode 100644 index ce614c9fb9..0000000000 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtectionTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.publication.protection - -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test -import org.junit.runner.RunWith -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.asset.ContainerAsset -import org.readium.r2.shared.util.checkSuccess -import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.mediatype.MediaType -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class LcpFallbackContentProtectionTest { - - @Test - fun `Sniff no content protection`() { - assertFalse(supports(mediaType = MediaType.EPUB, resources = emptyMap()).checkSuccess()) - } - - @Test - fun `Sniff EPUB with empty encryption xml`() { - assertFalse( - supports( - mediaType = MediaType.EPUB, - resources = mapOf( - "META-INF/encryption.xml" to """""" - ) - ).checkSuccess() - ) - } - - @Test - fun `Sniff LCP protected Readium package`() { - assertTrue( - supports( - mediaType = MediaType.READIUM_WEBPUB, - resources = mapOf( - "license.lcpl" to "{}" - ) - ).checkSuccess() - ) - } - - @Test - fun `Sniff LCP protected EPUB`() { - assertTrue( - supports( - mediaType = MediaType.EPUB, - resources = mapOf( - "META-INF/license.lcpl" to "{}" - ) - ).checkSuccess() - ) - } - - @Test - fun `Sniff LCP protected EPUB missing the license`() { - assertTrue( - supports( - mediaType = MediaType.EPUB, - resources = mapOf( - "META-INF/encryption.xml" to """ - - - - - - - - - - - - - - - -""" - ) - ).checkSuccess() - ) - } - - private fun supports(mediaType: MediaType, resources: Map): Try = runBlocking { - LcpFallbackContentProtection().supports( - ContainerAsset( - mediaType = mediaType, - container = TestContainer(resources.mapKeys { Url(it.key)!! }) - ) - ) - } -} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/format/DefaultSniffersTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/format/DefaultSniffersTest.kt new file mode 100644 index 0000000000..339da420c4 --- /dev/null +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/format/DefaultSniffersTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.format + +import kotlin.test.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.checkSuccess +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DefaultSniffersTest { + + @Test + fun `Adept sniffers doesn't recognize EPUB with empty encryption xml`() = runBlocking { + assertEquals( + Format.EPUB, + AdeptSniffer.sniffContainer( + Format.EPUB, + container = TestContainer( + Url("META-INF/encryption.xml")!! to """""" + ) + ).checkSuccess() + ) + } + + @Test + fun `Sniff Adobe ADEPT`() = runBlocking { + assertEquals( + Format.EPUB_ADEPT, + AdeptSniffer.sniffContainer( + format = Format.EPUB, + container = TestContainer( + Url("META-INF/encryption.xml")!! to """ + + + + urn:uuid:2c43729c-b985-4531-8e86-ae75ce5e5da9 + + + + + + """ + ) + ).checkSuccess() + ) + } + + @Test + fun `Sniff Adobe ADEPT from rights xml`() = runBlocking { + assertEquals( + Format.EPUB_ADEPT, + AdeptSniffer.sniffContainer( + format = Format.EPUB, + container = TestContainer( + Url("META-INF/encryption.xml")!! to """""", + Url("META-INF/rights.xml")!! to """""" + ) + ).checkSuccess() + ) + } + + @Test + fun `LcpSniffer doesn't recognize EPUB with empty encryption xml`() = runBlocking { + assertEquals( + Format.EPUB, + LcpSniffer.sniffContainer( + format = Format.EPUB, + container = TestContainer( + Url("META-INF/encryption.xml")!! to """""" + + ) + ).checkSuccess() + ) + } + + @Test + fun `Sniff LCP protected Readium package`() = runBlocking { + assertEquals( + Format.RPF_LCP, + LcpSniffer.sniffContainer( + format = Format.RPF, + container = TestContainer(Url("license.lcpl")!! to "{}") + ).checkSuccess() + ) + } + + @Test + fun `Sniff LCP protected EPUB`() = runBlocking { + assertEquals( + Format.EPUB_LCP, + LcpSniffer.sniffContainer( + format = Format.EPUB, + container = TestContainer(Url("META-INF/license.lcpl")!! to "{}") + ).checkSuccess() + ) + } + + @Test + fun `Sniff LCP protected EPUB missing the license`() = runBlocking { + assertEquals( + Format.EPUB_LCP, + LcpSniffer.sniffContainer( + format = Format.EPUB, + container = TestContainer( + Url("META-INF/encryption.xml")!! to """ + + + + + + + + + + + + + + + +""" + ) + ).checkSuccess() + ) + } +} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/format/TestContainer.kt similarity index 78% rename from readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt rename to readium/shared/src/test/java/org/readium/r2/shared/util/format/TestContainer.kt index 085f620231..5f6b02b8b3 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/publication/protection/TestContainer.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/format/TestContainer.kt @@ -4,7 +4,7 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.shared.publication.protection +package org.readium.r2.shared.util.format import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.data.Container @@ -15,6 +15,12 @@ class TestContainer( private val resources: Map = emptyMap() ) : Container { + companion object { + + operator fun invoke(vararg entry: Pair): TestContainer = + TestContainer(entry.toMap()) + } + override val entries: Set = resources.keys diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt new file mode 100644 index 0000000000..299a827c16 --- /dev/null +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt @@ -0,0 +1,570 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.mediatype + +import kotlin.test.assertEquals +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.readium.r2.shared.Fixtures +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.AssetSniffer +import org.readium.r2.shared.util.asset.SniffError +import org.readium.r2.shared.util.checkSuccess +import org.readium.r2.shared.util.data.EmptyContainer +import org.readium.r2.shared.util.format.FileExtension +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatHints +import org.readium.r2.shared.util.resource.StringResource +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class AssetSnifferTest { + + private val fixtures = Fixtures("util/asset") + + private val sniffer = AssetSniffer() + + private suspend fun AssetSniffer.sniffHints(formatHints: FormatHints): Try = + sniff( + hints = formatHints, + container = EmptyContainer() + ) + + private suspend fun AssetSniffer.sniffFileExtension(extension: String?): Try = + sniffHints(FormatHints(fileExtension = extension?.let { FileExtension((it)) })) + + private suspend fun AssetSniffer.sniffMediaType(mediaType: String?): Try = + sniffHints(FormatHints(mediaType = mediaType?.let { MediaType(it) })) + + @Test + fun `sniff ignores extension case`() = runBlocking { + assertEquals( + Format.EPUB, + sniffer.sniffFileExtension("EPUB").checkSuccess() + ) + } + + @Test + fun `sniff ignores media type case`() = runBlocking { + assertEquals( + Format.EPUB, + sniffer.sniffMediaType("APPLICATION/EPUB+ZIP").checkSuccess() + ) + } + + @Test + fun `sniff ignores media type extra parameters`() = runBlocking { + assertEquals( + Format.EPUB, + sniffer.sniffMediaType("application/epub+zip;param=value").checkSuccess() + ) + } + + @Test + fun `sniff from metadata`() = runBlocking { + assertEquals( + sniffer.sniffFileExtension(null).failureOrNull(), + SniffError.NotRecognized + ) + assertEquals( + Format.RPF_AUDIO, + sniffer.sniffFileExtension("audiobook").checkSuccess() + ) + assertEquals( + sniffer.sniffMediaType(null).failureOrNull(), + SniffError.NotRecognized + ) + assertEquals( + Format.RPF_AUDIO, + sniffer.sniffMediaType("application/audiobook+zip").checkSuccess() + ) + assertEquals( + Format.RPF_AUDIO, + sniffer.sniffHints( + FormatHints( + mediaTypes = listOf("application/audiobook+zip"), + fileExtensions = listOf("audiobook") + ) + ).checkSuccess() + ) + } + + @Test + fun `sniff from bytes`() = runBlocking { + assertEquals( + Format.RWPM_AUDIO, + sniffer.sniff(fixtures.fileAt("audiobook.json")).checkSuccess() + ) + } + + @Test + fun `sniff unknown format`() = runBlocking { + assertEquals( + SniffError.NotRecognized, + sniffer.sniffMediaType(mediaType = "invalid").failureOrNull() + ) + assertEquals( + SniffError.NotRecognized, + sniffer.sniff(fixtures.fileAt("unknown")).failureOrNull() + ) + } + + @Test + fun `sniff audiobook`() = runBlocking { + assertEquals( + Format.RPF_AUDIO, + sniffer.sniffFileExtension("audiobook").checkSuccess() + ) + assertEquals( + Format.RPF_AUDIO, + sniffer.sniffMediaType("application/audiobook+zip").checkSuccess() + ) + assertEquals( + Format.RPF_AUDIO, + sniffer.sniff(fixtures.fileAt("audiobook-package.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff audiobook manifest`() = runBlocking { + assertEquals( + Format.RWPM_AUDIO, + sniffer.sniffMediaType("application/audiobook+json").checkSuccess() + ) + assertEquals( + Format.RWPM_AUDIO, + sniffer.sniff(fixtures.fileAt("audiobook.json")).checkSuccess() + ) + assertEquals( + Format.RWPM_AUDIO, + sniffer.sniff(fixtures.fileAt("audiobook-wrongtype.json")).checkSuccess() + ) + } + + @Test + fun `sniff BMP`() = runBlocking { + assertEquals(Format.BMP, sniffer.sniffFileExtension("bmp").checkSuccess()) + assertEquals(Format.BMP, sniffer.sniffFileExtension("dib").checkSuccess()) + assertEquals(Format.BMP, sniffer.sniffMediaType("image/bmp").checkSuccess()) + assertEquals(Format.BMP, sniffer.sniffMediaType("image/x-bmp").checkSuccess()) + } + + @Test + fun `sniff CBZ`() = runBlocking { + assertEquals( + Format.CBZ, + sniffer.sniffFileExtension("cbz").checkSuccess() + ) + assertEquals( + Format.CBZ, + sniffer.sniffMediaType("application/vnd.comicbook+zip").checkSuccess() + ) + assertEquals( + Format.CBZ, + sniffer.sniffMediaType("application/x-cbz").checkSuccess() + ) + assertEquals( + Format.CBR, + sniffer.sniffMediaType("application/x-cbr").checkSuccess() + ) + assertEquals( + Format.CBZ, + sniffer.sniff(fixtures.fileAt("cbz.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff DiViNa`() = runBlocking { + assertEquals( + Format.RPF_IMAGE, + sniffer.sniffFileExtension("divina").checkSuccess() + ) + assertEquals( + Format.RPF_IMAGE, + sniffer.sniffMediaType("application/divina+zip").checkSuccess() + ) + assertEquals( + Format.RPF_IMAGE, + sniffer.sniff(fixtures.fileAt("divina-package.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff DiViNa manifest`() = runBlocking { + assertEquals( + Format.RWPM_IMAGE, + sniffer.sniffMediaType("application/divina+json").checkSuccess() + ) + assertEquals( + Format.RWPM_IMAGE, + sniffer.sniff(fixtures.fileAt("divina.json")).checkSuccess() + ) + } + + @Test + fun `sniff EPUB`() = runBlocking { + assertEquals( + Format.EPUB, + sniffer.sniffFileExtension("epub").checkSuccess() + ) + assertEquals( + Format.EPUB, + sniffer.sniffMediaType("application/epub+zip").checkSuccess() + ) + assertEquals( + Format.EPUB, + sniffer.sniff(fixtures.fileAt("epub.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff AVIF`() = runBlocking { + assertEquals(Format.AVIF, sniffer.sniffFileExtension("avif").checkSuccess()) + assertEquals(Format.AVIF, sniffer.sniffMediaType("image/avif").checkSuccess()) + } + + @Test + fun `sniff GIF`() = runBlocking { + assertEquals(Format.GIF, sniffer.sniffFileExtension("gif").checkSuccess()) + assertEquals(Format.GIF, sniffer.sniffMediaType("image/gif").checkSuccess()) + } + + @Test + fun `sniff HTML`() = runBlocking { + assertEquals( + Format.HTML, + sniffer.sniffFileExtension("htm").checkSuccess() + ) + assertEquals( + Format.HTML, + sniffer.sniffFileExtension("html").checkSuccess() + ) + assertEquals( + Format.HTML, + sniffer.sniffMediaType("text/html").checkSuccess() + ) + assertEquals( + Format.HTML, + sniffer.sniff(fixtures.fileAt("html.unknown")).checkSuccess() + ) + assertEquals( + Format.HTML, + sniffer.sniff(fixtures.fileAt("html-doctype-case.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff XHTML`() = runBlocking { + assertEquals( + Format.XHTML, + sniffer.sniffFileExtension("xht").checkSuccess() + ) + assertEquals( + Format.XHTML, + sniffer.sniffFileExtension("xhtml").checkSuccess() + ) + assertEquals( + Format.XHTML, + sniffer.sniffMediaType("application/xhtml+xml").checkSuccess() + ) + assertEquals( + Format.XHTML, + sniffer.sniff(fixtures.fileAt("xhtml.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff JPEG`() = runBlocking { + assertEquals(Format.JPEG, sniffer.sniffFileExtension("jpg").checkSuccess()) + assertEquals(Format.JPEG, sniffer.sniffFileExtension("jpeg").checkSuccess()) + assertEquals(Format.JPEG, sniffer.sniffFileExtension("jpe").checkSuccess()) + assertEquals(Format.JPEG, sniffer.sniffFileExtension("jif").checkSuccess()) + assertEquals(Format.JPEG, sniffer.sniffFileExtension("jfif").checkSuccess()) + assertEquals(Format.JPEG, sniffer.sniffFileExtension("jfi").checkSuccess()) + assertEquals(Format.JPEG, sniffer.sniffMediaType("image/jpeg").checkSuccess()) + } + + @Test + fun `sniff JXL`() = runBlocking { + assertEquals(Format.JXL, sniffer.sniffFileExtension("jxl").checkSuccess()) + assertEquals(Format.JXL, sniffer.sniffMediaType("image/jxl").checkSuccess()) + } + + @Test + fun `sniff RAR`() = runBlocking { + assertEquals( + Format.RAR, + sniffer.sniffFileExtension("rar").checkSuccess() + ) + assertEquals( + Format.RAR, + sniffer.sniffMediaType("application/vnd.rar").checkSuccess() + ) + assertEquals( + Format.RAR, + sniffer.sniffMediaType("application/x-rar").checkSuccess() + ) + assertEquals( + Format.RAR, + sniffer.sniffMediaType("application/x-rar-compressed").checkSuccess() + ) + } + + @Test + fun `sniff OPDS 1 feed`() = runBlocking { + assertEquals( + Format.OPDS1, + sniffer.sniffMediaType("application/atom+xml;profile=opds-catalog").checkSuccess() + ) + assertEquals( + Format.OPDS1_NAVIGATION_FEED, + sniffer.sniffMediaType("application/atom+xml;profile=opds-catalog;kind=navigation").checkSuccess() + ) + assertEquals( + Format.OPDS1_ACQUISITION_FEED, + sniffer.sniffMediaType("application/atom+xml;profile=opds-catalog;kind=acquisition").checkSuccess() + ) + assertEquals( + Format.OPDS1, + sniffer.sniff(fixtures.fileAt("opds1-feed.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff OPDS 1 entry`() = runBlocking { + assertEquals( + Format.OPDS1_ENTRY, + sniffer.sniffMediaType("application/atom+xml;type=entry;profile=opds-catalog").checkSuccess() + ) + assertEquals( + Format.OPDS1_ENTRY, + sniffer.sniff(fixtures.fileAt("opds1-entry.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff OPDS 2 feed`() = runBlocking { + assertEquals( + Format.OPDS2, + sniffer.sniffMediaType("application/opds+json").checkSuccess() + ) + assertEquals( + Format.OPDS2, + sniffer.sniff(fixtures.fileAt("opds2-feed.json")).checkSuccess() + ) + } + + @Test + fun `sniff OPDS 2 publication`() = runBlocking { + assertEquals( + Format.OPDS2_PUBLICATION, + sniffer.sniffMediaType("application/opds-publication+json").checkSuccess() + ) + assertEquals( + Format.OPDS2_PUBLICATION, + sniffer.sniff(fixtures.fileAt("opds2-publication.json")).checkSuccess() + ) + } + + @Test + fun `sniff OPDS authentication document`() = runBlocking { + assertEquals( + Format.OPDS_AUTHENTICATION, + sniffer.sniffMediaType("application/opds-authentication+json").checkSuccess() + ) + assertEquals( + Format.OPDS_AUTHENTICATION, + sniffer.sniffMediaType("application/vnd.opds.authentication.v1.0+json").checkSuccess() + ) + assertEquals( + Format.OPDS_AUTHENTICATION, + sniffer.sniff(fixtures.fileAt("opds-authentication.json")).checkSuccess() + ) + } + + @Test + fun `sniff LCP protected audiobook`() = runBlocking { + assertEquals( + Format.RPF_AUDIO_LCP, + sniffer.sniffFileExtension("lcpa").checkSuccess() + ) + assertEquals( + Format.RPF_AUDIO_LCP, + sniffer.sniffMediaType("application/audiobook+lcp").checkSuccess() + ) + assertEquals( + Format.RPF_AUDIO_LCP, + sniffer.sniff(fixtures.fileAt("audiobook-lcp.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff LCP protected PDF`() = runBlocking { + assertEquals( + Format.RPF_PDF_LCP, + sniffer.sniffFileExtension("lcpdf").checkSuccess() + ) + assertEquals( + Format.RPF_PDF_LCP, + sniffer.sniffMediaType("application/pdf+lcp").checkSuccess() + ) + assertEquals( + Format.RPF_PDF_LCP, + sniffer.sniff(fixtures.fileAt("pdf-lcp.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff LCP license document`() = runBlocking { + assertEquals( + Format.LCP_LICENSE_DOCUMENT, + sniffer.sniffFileExtension("lcpl").checkSuccess() + ) + assertEquals( + Format.LCP_LICENSE_DOCUMENT, + sniffer.sniffMediaType("application/vnd.readium.lcp.license.v1.0+json").checkSuccess() + ) + assertEquals( + Format.LCP_LICENSE_DOCUMENT, + sniffer.sniff(fixtures.fileAt("lcpl.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff LPF`() = runBlocking { + assertEquals( + Format.LPF, + sniffer.sniffFileExtension("lpf").checkSuccess() + ) + assertEquals( + Format.LPF, + sniffer.sniffMediaType("application/lpf+zip").checkSuccess() + ) + assertEquals( + Format.LPF, + sniffer.sniff(fixtures.fileAt("lpf.unknown")).checkSuccess() + ) + assertEquals( + Format.LPF, + sniffer.sniff(fixtures.fileAt("lpf-index-html.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff PDF`() = runBlocking { + assertEquals( + Format.PDF, + sniffer.sniffFileExtension("pdf").checkSuccess() + ) + assertEquals( + Format.PDF, + sniffer.sniffMediaType("application/pdf").checkSuccess() + ) + assertEquals( + Format.PDF, + sniffer.sniff(fixtures.fileAt("pdf.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff PNG`() = runBlocking { + assertEquals(Format.PNG, sniffer.sniffFileExtension("png").checkSuccess()) + assertEquals(Format.PNG, sniffer.sniffMediaType("image/png").checkSuccess()) + } + + @Test + fun `sniff TIFF`() = runBlocking { + assertEquals(Format.TIFF, sniffer.sniffFileExtension("tiff").checkSuccess()) + assertEquals(Format.TIFF, sniffer.sniffFileExtension("tif").checkSuccess()) + assertEquals(Format.TIFF, sniffer.sniffMediaType("image/tiff").checkSuccess()) + assertEquals(Format.TIFF, sniffer.sniffMediaType("image/tiff-fx").checkSuccess()) + } + + @Test + fun `sniff WebP`() = runBlocking { + assertEquals(Format.WEBP, sniffer.sniffFileExtension("webp").checkSuccess()) + assertEquals(Format.WEBP, sniffer.sniffMediaType("image/webp").checkSuccess()) + } + + @Test + fun `sniff WebPub`() = runBlocking { + assertEquals( + Format.RPF, + sniffer.sniffFileExtension("webpub").checkSuccess() + ) + assertEquals( + Format.RPF, + sniffer.sniffMediaType("application/webpub+zip").checkSuccess() + ) + assertEquals( + Format.RPF, + sniffer.sniff(fixtures.fileAt("webpub-package.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff WebPub manifest`() = runBlocking { + assertEquals( + Format.RWPM, + sniffer.sniffMediaType("application/webpub+json").checkSuccess() + ) + assertEquals( + Format.RWPM, + sniffer.sniff(fixtures.fileAt("webpub.json")).checkSuccess() + ) + } + + @Test + fun `sniff W3C WPUB manifest`() = runBlocking { + assertEquals( + Format.W3C_WPUB_MANIFEST, + sniffer.sniff(fixtures.fileAt("w3c-wpub.json")).checkSuccess() + ) + } + + @Test + fun `sniff ZAB`() = runBlocking { + assertEquals( + Format.ZAB, + sniffer.sniffFileExtension("zab").checkSuccess() + ) + assertEquals( + Format.ZAB, + sniffer.sniff(fixtures.fileAt("zab.unknown")).checkSuccess() + ) + } + + @Test + fun `sniff JSON`() = runBlocking { + assertEquals( + Format.JSON, + sniffer.sniff(fixtures.fileAt("any.json")).checkSuccess() + ) + } + + @Test + fun `sniff JSON problem details`() = runBlocking { + assertEquals( + Format.JSON_PROBLEM_DETAILS, + sniffer.sniffMediaType("application/problem+json").checkSuccess() + ) + assertEquals( + Format.JSON_PROBLEM_DETAILS, + sniffer.sniffMediaType("application/problem+json; charset=utf-8").checkSuccess() + ) + + // The sniffing of a JSON document should not take precedence over the JSON problem details. + assertEquals( + Format.JSON_PROBLEM_DETAILS, + sniffer.sniff( + source = StringResource("""{"title": "Message"}"""), + hints = FormatHints(mediaType = MediaType("application/problem+json")!!) + ).checkSuccess() + ) + } +} diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt index 0562a20e98..540cd71de1 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt @@ -3,7 +3,9 @@ package org.readium.r2.shared.util.mediatype import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking import org.junit.Test +import org.readium.r2.shared.util.format.FileExtension import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatInfo import org.readium.r2.shared.util.format.FormatRegistry class FormatRegistryTest { @@ -14,25 +16,28 @@ class FormatRegistryTest { fun `get known file extension from format`() = runBlocking { assertEquals( "epub", - sut()[Format.EPUB]?.fileExtension.value + sut()[Format.EPUB]?.fileExtension?.value ) } @Test - fun `register new file extensions`() = runBlocking { - val mediaType = MediaType("application/test")!! - val sut = sut() - sut.register(mediaType, fileExtension = "tst", superType = null) - - assertEquals(sut.fileExtension(mediaType), "tst") + fun `get known media type from format`() = runBlocking { + assertEquals( + "application/epub+zip", + sut()[Format.EPUB]?.mediaType.toString() + ) } @Test - fun `register new format with supertype`() = runBlocking { + fun `register new format`() = runBlocking { val mediaType = MediaType("application/test")!! val sut = sut() - sut.register(mediaType, fileExtension = null, superType = MediaType.ZIP) - - assertEquals(sut.superType(mediaType), MediaType.ZIP) + val format = Format("tst") + val formatInfo = FormatInfo( + mediaType = mediaType, + fileExtension = FileExtension("tst") + ) + sut.register(format, formatInfo) + assertEquals(sut[format], formatInfo) } } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt deleted file mode 100644 index 38582722e6..0000000000 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/MediaTypeRetrieverTest.kt +++ /dev/null @@ -1,537 +0,0 @@ -package org.readium.r2.shared.util.mediatype - -import android.webkit.MimeTypeMap -import kotlin.test.assertEquals -import kotlin.test.assertNull -import kotlinx.coroutines.runBlocking -import org.junit.Test -import org.junit.runner.RunWith -import org.readium.r2.shared.Fixtures -import org.readium.r2.shared.util.checkSuccess -import org.readium.r2.shared.util.format.FormatRegistry -import org.readium.r2.shared.util.resource.StringResource -import org.readium.r2.shared.util.sniff.SniffError -import org.readium.r2.shared.util.zip.ZipArchiveOpener -import org.robolectric.RobolectricTestRunner -import org.robolectric.Shadows.shadowOf - -@RunWith(RobolectricTestRunner::class) -class MediaTypeRetrieverTest { - - val fixtures = Fixtures("util/mediatype") - - private val retriever = MediaTypeRetriever( - DefaultMediaTypeSniffer(), - FormatRegistry(), - ZipArchiveOpener() - ) - - @Test - fun `sniff ignores extension case`() = runBlocking { - assertEquals(MediaType.EPUB, retriever.retrieve(fileExtension = "EPUB")) - } - - @Test - fun `sniff ignores media type case`() = runBlocking { - assertEquals( - MediaType.EPUB, - retriever.retrieve(mediaType = "APPLICATION/EPUB+ZIP") - ) - } - - @Test - fun `sniff ignores media type extra parameters`() = runBlocking { - assertEquals( - MediaType.EPUB, - retriever.retrieve(mediaType = "application/epub+zip;param=value") - ) - } - - @Test - fun `sniff from metadata`() = runBlocking { - assertNull(retriever.retrieve(fileExtension = null)) - assertEquals( - MediaType.READIUM_AUDIOBOOK, - retriever.retrieve(fileExtension = "audiobook") - ) - assertNull(retriever.retrieve(mediaType = null)) - assertEquals( - MediaType.READIUM_AUDIOBOOK, - retriever.retrieve(mediaType = "application/audiobook+zip") - ) - assertEquals( - MediaType.READIUM_AUDIOBOOK, - retriever.retrieve(mediaType = "application/audiobook+zip") - ) - assertEquals( - MediaType.READIUM_AUDIOBOOK, - retriever.retrieve( - mediaType = "application/audiobook+zip", - fileExtension = "audiobook" - ) - ) - assertEquals( - MediaType.READIUM_AUDIOBOOK, - retriever.retrieve( - MediaTypeHints( - mediaTypes = listOf("application/audiobook+zip"), - fileExtensions = listOf("audiobook") - ) - ) - ) - } - - @Test - fun `sniff from bytes`() = runBlocking { - assertEquals( - MediaType.READIUM_AUDIOBOOK_MANIFEST, - retriever.retrieve(fixtures.fileAt("audiobook.json")).checkSuccess() - ) - } - - @Test - fun `sniff unknown format`() = runBlocking { - assertNull(retriever.retrieve(mediaType = "invalid")) - assertEquals( - retriever.retrieve(fixtures.fileAt("unknown")).failureOrNull(), - SniffError.NotRecognized - ) - } - - @Test - fun `sniff audiobook`() = runBlocking { - assertEquals( - MediaType.READIUM_AUDIOBOOK, - retriever.retrieve(fileExtension = "audiobook") - ) - assertEquals( - MediaType.READIUM_AUDIOBOOK, - retriever.retrieve(mediaType = "application/audiobook+zip") - ) - assertEquals( - MediaType.READIUM_AUDIOBOOK, - retriever.retrieve(fixtures.fileAt("audiobook-package.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff audiobook manifest`() = runBlocking { - assertEquals( - MediaType.READIUM_AUDIOBOOK_MANIFEST, - retriever.retrieve(mediaType = "application/audiobook+json") - ) - assertEquals( - MediaType.READIUM_AUDIOBOOK_MANIFEST, - retriever.retrieve(fixtures.fileAt("audiobook.json")).checkSuccess() - ) - assertEquals( - MediaType.READIUM_AUDIOBOOK_MANIFEST, - retriever.retrieve(fixtures.fileAt("audiobook-wrongtype.json")).checkSuccess() - ) - } - - @Test - fun `sniff BMP`() = runBlocking { - assertEquals(MediaType.BMP, retriever.retrieve(fileExtension = "bmp")) - assertEquals(MediaType.BMP, retriever.retrieve(fileExtension = "dib")) - assertEquals(MediaType.BMP, retriever.retrieve(mediaType = "image/bmp")) - assertEquals(MediaType.BMP, retriever.retrieve(mediaType = "image/x-bmp")) - } - - @Test - fun `sniff CBZ`() = runBlocking { - assertEquals(MediaType.CBZ, retriever.retrieve(fileExtension = "cbz")) - assertEquals( - MediaType.CBZ, - retriever.retrieve(mediaType = "application/vnd.comicbook+zip") - ) - assertEquals(MediaType.CBZ, retriever.retrieve(mediaType = "application/x-cbz")) - assertEquals(MediaType.CBR, retriever.retrieve(mediaType = "application/x-cbr")) - - assertEquals( - MediaType.CBZ, - retriever.retrieve(fixtures.fileAt("cbz.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff DiViNa`() = runBlocking { - assertEquals(MediaType.DIVINA, retriever.retrieve(fileExtension = "divina")) - assertEquals( - MediaType.DIVINA, - retriever.retrieve(mediaType = "application/divina+zip") - ) - assertEquals( - MediaType.DIVINA, - retriever.retrieve(fixtures.fileAt("divina-package.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff DiViNa manifest`() = runBlocking { - assertEquals( - MediaType.DIVINA_MANIFEST, - retriever.retrieve(mediaType = "application/divina+json") - ) - assertEquals( - MediaType.DIVINA_MANIFEST, - retriever.retrieve(fixtures.fileAt("divina.json")).checkSuccess() - ) - } - - @Test - fun `sniff EPUB`() = runBlocking { - assertEquals(MediaType.EPUB, retriever.retrieve(fileExtension = "epub")) - assertEquals( - MediaType.EPUB, - retriever.retrieve(mediaType = "application/epub+zip") - ) - assertEquals( - MediaType.EPUB, - retriever.retrieve(fixtures.fileAt("epub.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff AVIF`() = runBlocking { - assertEquals(MediaType.AVIF, retriever.retrieve(fileExtension = "avif")) - assertEquals(MediaType.AVIF, retriever.retrieve(mediaType = "image/avif")) - } - - @Test - fun `sniff GIF`() = runBlocking { - assertEquals(MediaType.GIF, retriever.retrieve(fileExtension = "gif")) - assertEquals(MediaType.GIF, retriever.retrieve(mediaType = "image/gif")) - } - - @Test - fun `sniff HTML`() = runBlocking { - assertEquals(MediaType.HTML, retriever.retrieve(fileExtension = "htm")) - assertEquals(MediaType.HTML, retriever.retrieve(fileExtension = "html")) - assertEquals(MediaType.HTML, retriever.retrieve(mediaType = "text/html")) - assertEquals( - MediaType.HTML, - retriever.retrieve(fixtures.fileAt("html.unknown")).checkSuccess() - ) - assertEquals( - MediaType.HTML, - retriever.retrieve(fixtures.fileAt("html-doctype-case.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff XHTML`() = runBlocking { - assertEquals(MediaType.XHTML, retriever.retrieve(fileExtension = "xht")) - assertEquals(MediaType.XHTML, retriever.retrieve(fileExtension = "xhtml")) - assertEquals( - MediaType.XHTML, - retriever.retrieve(mediaType = "application/xhtml+xml") - ) - assertEquals( - MediaType.XHTML, - retriever.retrieve(fixtures.fileAt("xhtml.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff JPEG`() = runBlocking { - assertEquals(MediaType.JPEG, retriever.retrieve(fileExtension = "jpg")) - assertEquals(MediaType.JPEG, retriever.retrieve(fileExtension = "jpeg")) - assertEquals(MediaType.JPEG, retriever.retrieve(fileExtension = "jpe")) - assertEquals(MediaType.JPEG, retriever.retrieve(fileExtension = "jif")) - assertEquals(MediaType.JPEG, retriever.retrieve(fileExtension = "jfif")) - assertEquals(MediaType.JPEG, retriever.retrieve(fileExtension = "jfi")) - assertEquals(MediaType.JPEG, retriever.retrieve(mediaType = "image/jpeg")) - } - - @Test - fun `sniff JXL`() = runBlocking { - assertEquals(MediaType.JXL, retriever.retrieve(fileExtension = "jxl")) - assertEquals(MediaType.JXL, retriever.retrieve(mediaType = "image/jxl")) - } - - @Test - fun `sniff RAR`() = runBlocking { - assertEquals(MediaType.RAR, retriever.retrieve(fileExtension = "rar")) - assertEquals(MediaType.RAR, retriever.retrieve(mediaType = "application/vnd.rar")) - assertEquals(MediaType.RAR, retriever.retrieve(mediaType = "application/x-rar")) - assertEquals(MediaType.RAR, retriever.retrieve(mediaType = "application/x-rar-compressed")) - } - - @Test - fun `sniff OPDS 1 feed`() = runBlocking { - assertEquals( - MediaType.OPDS1, - retriever.retrieve(mediaType = "application/atom+xml;profile=opds-catalog") - ) - assertEquals( - MediaType.OPDS1_NAVIGATION_FEED, - retriever.retrieve("application/atom+xml;profile=opds-catalog;kind=navigation") - ) - assertEquals( - MediaType.OPDS1_ACQUISITION_FEED, - retriever.retrieve("application/atom+xml;profile=opds-catalog;kind=acquisition") - ) - assertEquals( - MediaType.OPDS1, - retriever.retrieve(fixtures.fileAt("opds1-feed.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff OPDS 1 entry`() = runBlocking { - assertEquals( - MediaType.OPDS1_ENTRY, - retriever.retrieve( - mediaType = "application/atom+xml;type=entry;profile=opds-catalog" - ) - ) - assertEquals( - MediaType.OPDS1_ENTRY, - retriever.retrieve(fixtures.fileAt("opds1-entry.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff OPDS 2 feed`() = runBlocking { - assertEquals( - MediaType.OPDS2, - retriever.retrieve(mediaType = "application/opds+json") - ) - assertEquals( - MediaType.OPDS2, - retriever.retrieve(fixtures.fileAt("opds2-feed.json")).checkSuccess() - ) - } - - @Test - fun `sniff OPDS 2 publication`() = runBlocking { - assertEquals( - MediaType.OPDS2_PUBLICATION, - retriever.retrieve(mediaType = "application/opds-publication+json") - ) - assertEquals( - MediaType.OPDS2_PUBLICATION, - retriever.retrieve(fixtures.fileAt("opds2-publication.json")).checkSuccess() - ) - } - - @Test - fun `sniff OPDS authentication document`() = runBlocking { - assertEquals( - MediaType.OPDS_AUTHENTICATION, - retriever.retrieve(mediaType = "application/opds-authentication+json") - ) - assertEquals( - MediaType.OPDS_AUTHENTICATION, - retriever.retrieve(mediaType = "application/vnd.opds.authentication.v1.0+json") - ) - assertEquals( - MediaType.OPDS_AUTHENTICATION, - retriever.retrieve(fixtures.fileAt("opds-authentication.json")).checkSuccess() - ) - } - - @Test - fun `sniff LCP protected audiobook`() = runBlocking { - assertEquals( - MediaType.LCP_PROTECTED_AUDIOBOOK, - retriever.retrieve(fileExtension = "lcpa") - ) - assertEquals( - MediaType.LCP_PROTECTED_AUDIOBOOK, - retriever.retrieve(mediaType = "application/audiobook+lcp") - ) - assertEquals( - MediaType.LCP_PROTECTED_AUDIOBOOK, - retriever.retrieve(fixtures.fileAt("audiobook-lcp.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff LCP protected PDF`() = runBlocking { - assertEquals( - MediaType.LCP_PROTECTED_PDF, - retriever.retrieve(fileExtension = "lcpdf") - ) - assertEquals( - MediaType.LCP_PROTECTED_PDF, - retriever.retrieve(mediaType = "application/pdf+lcp") - ) - assertEquals( - MediaType.LCP_PROTECTED_PDF, - retriever.retrieve(fixtures.fileAt("pdf-lcp.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff LCP license document`() = runBlocking { - assertEquals( - MediaType.LCP_LICENSE_DOCUMENT, - retriever.retrieve(fileExtension = "lcpl") - ) - assertEquals( - MediaType.LCP_LICENSE_DOCUMENT, - retriever.retrieve(mediaType = "application/vnd.readium.lcp.license.v1.0+json") - ) - assertEquals( - MediaType.LCP_LICENSE_DOCUMENT, - retriever.retrieve(fixtures.fileAt("lcpl.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff LPF`() = runBlocking { - assertEquals(MediaType.LPF, retriever.retrieve(fileExtension = "lpf")) - assertEquals(MediaType.LPF, retriever.retrieve(mediaType = "application/lpf+zip")) - assertEquals( - MediaType.LPF, - retriever.retrieve(fixtures.fileAt("lpf.unknown")).checkSuccess() - ) - assertEquals( - MediaType.LPF, - retriever.retrieve(fixtures.fileAt("lpf-index-html.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff PDF`() = runBlocking { - assertEquals(MediaType.PDF, retriever.retrieve(fileExtension = "pdf")) - assertEquals(MediaType.PDF, retriever.retrieve(mediaType = "application/pdf")) - assertEquals( - MediaType.PDF, - retriever.retrieve(fixtures.fileAt("pdf.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff PNG`() = runBlocking { - assertEquals(MediaType.PNG, retriever.retrieve(fileExtension = "png")) - assertEquals(MediaType.PNG, retriever.retrieve(mediaType = "image/png")) - } - - @Test - fun `sniff TIFF`() = runBlocking { - assertEquals(MediaType.TIFF, retriever.retrieve(fileExtension = "tiff")) - assertEquals(MediaType.TIFF, retriever.retrieve(fileExtension = "tif")) - assertEquals(MediaType.TIFF, retriever.retrieve(mediaType = "image/tiff")) - assertEquals(MediaType.TIFF, retriever.retrieve(mediaType = "image/tiff-fx")) - } - - @Test - fun `sniff WebP`() = runBlocking { - assertEquals(MediaType.WEBP, retriever.retrieve(fileExtension = "webp")) - assertEquals(MediaType.WEBP, retriever.retrieve(mediaType = "image/webp")) - } - - @Test - fun `sniff WebPub`() = runBlocking { - assertEquals( - MediaType.READIUM_WEBPUB, - retriever.retrieve(fileExtension = "webpub") - ) - assertEquals( - MediaType.READIUM_WEBPUB, - retriever.retrieve(mediaType = "application/webpub+zip") - ) - assertEquals( - MediaType.READIUM_WEBPUB, - retriever.retrieve(fixtures.fileAt("webpub-package.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff WebPub manifest`() = runBlocking { - assertEquals( - MediaType.READIUM_WEBPUB_MANIFEST, - retriever.retrieve(mediaType = "application/webpub+json") - ) - assertEquals( - MediaType.READIUM_WEBPUB_MANIFEST, - retriever.retrieve(fixtures.fileAt("webpub.json")).checkSuccess() - ) - } - - @Test - fun `sniff W3C WPUB manifest`() = runBlocking { - assertEquals( - MediaType.W3C_WPUB_MANIFEST, - retriever.retrieve(fixtures.fileAt("w3c-wpub.json")).checkSuccess() - ) - } - - @Test - fun `sniff ZAB`() = runBlocking { - assertEquals(MediaType.ZAB, retriever.retrieve(fileExtension = "zab")) - assertEquals( - MediaType.ZAB, - retriever.retrieve(fixtures.fileAt("zab.unknown")).checkSuccess() - ) - } - - @Test - fun `sniff JSON`() = runBlocking { - assertEquals( - MediaType.JSON, - retriever.retrieve(fixtures.fileAt("any.json")).checkSuccess() - ) - } - - @Test - fun `sniff JSON problem details`() = runBlocking { - assertEquals( - MediaType.JSON_PROBLEM_DETAILS, - retriever.retrieve(mediaType = "application/problem+json") - ) - assertEquals( - MediaType.JSON_PROBLEM_DETAILS, - retriever.retrieve(mediaType = "application/problem+json; charset=utf-8") - ) - - // The sniffing of a JSON document should not take precedence over the JSON problem details. - assertEquals( - MediaType.JSON_PROBLEM_DETAILS, - retriever.retrieve( - resource = StringResource("""{"title": "Message"}"""), - hints = MediaTypeHints(mediaType = MediaType("application/problem+json")!!) - ).checkSuccess() - ) - } - - @Test - fun `sniff system media types`() = runBlocking { - shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping( - "xlsx", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ) - val xlsx = MediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")!! - assertEquals( - xlsx, - retriever.retrieve( - MediaTypeHints( - mediaTypes = emptyList(), - fileExtensions = listOf("foobar", "xlsx") - ) - ) - ) - assertEquals( - xlsx, - retriever.retrieve( - MediaTypeHints( - mediaTypes = listOf( - "applicaton/foobar", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ), - fileExtensions = emptyList() - ) - ) - ) - } - - @Test - fun `sniff system media types from bytes`() = runBlocking { - shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("png", "image/png") - val png = MediaType("image/png")!! - assertEquals(png, retriever.retrieve(fixtures.fileAt("png.unknown")).checkSuccess()) - } -} diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/any.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/any.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/any.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/any.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/audiobook-lcp.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook-lcp.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/audiobook-lcp.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook-lcp.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/audiobook-package.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook-package.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/audiobook-package.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook-package.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/audiobook-wrongtype.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook-wrongtype.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/audiobook-wrongtype.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook-wrongtype.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/audiobook.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/audiobook.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/audiobook.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/cbz.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/cbz.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/cbz.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/cbz.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/divina-package.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/divina-package.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/divina-package.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/divina-package.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/divina.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/divina.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/divina.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/divina.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/epub.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/epub.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/epub.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/epub.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/html-doctype-case.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/html-doctype-case.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/html-doctype-case.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/html-doctype-case.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/html.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/html.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/html.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/html.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/lcpl.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/lcpl.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/lcpl.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/lcpl.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/lpf-index-html.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/lpf-index-html.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/lpf-index-html.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/lpf-index-html.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/lpf.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/lpf.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/lpf.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/lpf.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/opds-authentication.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds-authentication.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/opds-authentication.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds-authentication.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/opds1-entry.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds1-entry.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/opds1-entry.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds1-entry.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/opds1-feed.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds1-feed.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/opds1-feed.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds1-feed.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/opds2-feed.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds2-feed.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/opds2-feed.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds2-feed.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/opds2-publication.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds2-publication.json similarity index 94% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/opds2-publication.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds2-publication.json index bb0b7b0191..e45578910d 100644 --- a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/opds2-publication.json +++ b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/opds2-publication.json @@ -8,7 +8,7 @@ "modified": "2015-09-29T17:00:00Z" }, "links": [ - {"rel": "self", "href": "http://example.org/manifest.json", "type": "application/webpub+json"}, + {"rel": "self", "href": "http://example.org/manifest.json", "type": "application/opds-publication+json"}, { "href": "/buy", "rel": "http://opds-spec.org/acquisition/buy", diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/pdf-lcp.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/pdf-lcp.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/pdf-lcp.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/pdf-lcp.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/pdf.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/pdf.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/pdf.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/pdf.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/png.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/png.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/png.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/png.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/unknown.zip b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/unknown.zip similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/unknown.zip rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/unknown.zip diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/w3c-wpub.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/w3c-wpub.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/w3c-wpub.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/w3c-wpub.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/webpub-package.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/webpub-package.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/webpub-package.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/webpub-package.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/webpub.json b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/webpub.json similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/webpub.json rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/webpub.json diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/xhtml.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/xhtml.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/xhtml.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/xhtml.unknown diff --git a/readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/zab.unknown b/readium/shared/src/test/resources/org/readium/r2/shared/util/asset/zab.unknown similarity index 100% rename from readium/shared/src/test/resources/org/readium/r2/shared/util/mediatype/zab.unknown rename to readium/shared/src/test/resources/org/readium/r2/shared/util/asset/zab.unknown diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index 4d8b9f16e5..4a96be9016 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -22,7 +22,6 @@ import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.file.FileResource -import org.readium.r2.shared.util.format.DefaultContentSniffer import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType @@ -38,7 +37,7 @@ class ImageParserTest { private val archiveOpener = ZipArchiveOpener() - private val assetSniffer = AssetSniffer(DefaultContentSniffer, archiveOpener) + private val assetSniffer = AssetSniffer() private val formatRegistry = FormatRegistry() From f20b32197470a7905e9c2a5abb0c9d485ff218de Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 13 Dec 2023 15:01:14 +0100 Subject: [PATCH 75/86] Add caching --- .../readium/r2/shared/util/data/Caching.kt | 88 +++++++++++++++++++ .../readium/r2/shared/util/data/Decoding.kt | 22 +++-- 2 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/data/Caching.kt diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Caching.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Caching.kt new file mode 100644 index 0000000000..8274928b45 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Caching.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2023 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.shared.util.data + +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.Url + +internal class CachingReadable( + private val source: Readable +) : Readable by source { + + private var startCache: ByteArray? = null + + private var contentLength: Long? = null + + override suspend fun length(): Try { + contentLength?.let { Try.success(it) } + + return source.length() + .onSuccess { contentLength = it } + } + + override suspend fun read(range: LongRange?): Try { + return when { + startCache == null -> { + source.read(range) + .onSuccess { + if (range == null || range.first == 0L) { + startCache = it + } + } + } + range == null -> { + if (contentLength == startCache!!.size.toLong()) { + Try.success(startCache!!) + } else { + source.read() + .onSuccess { + startCache = it + contentLength = it.size.toLong() + } + } + } + range.first == 0L -> { + if (range.last < startCache!!.size) { + Try.success(startCache!!.sliceArray(0..range.last.toInt())) + } else { + source.read(range) + .onSuccess { startCache = it } + } + } + else -> + return source.read(range) + } + } + + override suspend fun close() {} +} + +internal class CachingContainer( + private val container: Container +) : Container by container { + + private val cache: MutableMap = + mutableMapOf() + + override fun get(url: Url): Readable? { + cache[url]?.let { return it } + + val entry = container[url] + ?: return null + + val blobContext = CachingReadable(entry) + + cache[url] = blobContext + + return blobContext + } + + override suspend fun close() { + cache.forEach { it.value.close() } + cache.clear() + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt index 72953917e7..c2d0ddb180 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/data/Decoding.kt @@ -100,15 +100,19 @@ public suspend fun ByteArray.decodeJson(): Try = * Readium Web Publication Manifest parsed from the content. */ public suspend fun ByteArray.decodeRwpm(): Try = - decodeJson().flatMap { json -> - Manifest.fromJSON(json) - ?.let { Try.success(it) } - ?: Try.failure( - DecodeError.Decoding( - DebugError("Content is not a valid RWPM.") - ) - ) - } + decodeJson().flatMap { it.decodeRwpm() } + +/** + * Readium Web Publication Manifest parsed from JSON. + */ +public suspend fun JSONObject.decodeRwpm(): Try = + decode( + { + Manifest.fromJSON(this) + ?: throw Exception("Manifest.fromJSON returned null") + }, + { DebugError("Content is not a valid RWPM.") } + ) /** * Reads the full content as a [Bitmap]. From 4f959a13ce2e56679b839db4169a24d7aa143d30 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 13 Dec 2023 16:06:25 +0100 Subject: [PATCH 76/86] Remove ParserAssetFactory --- .../readium/r2/streamer/ParserAssetFactory.kt | 151 ------------------ 1 file changed, 151 deletions(-) delete mode 100644 readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt deleted file mode 100644 index f0cbe1a0cc..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.streamer - -import org.readium.r2.shared.extensions.addPrefix -import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.DebugError -import org.readium.r2.shared.util.Error -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.asset.ContainerAsset -import org.readium.r2.shared.util.asset.ResourceAsset -import org.readium.r2.shared.util.data.CompositeContainer -import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.data.decodeRwpm -import org.readium.r2.shared.util.data.readDecodeOrElse -import org.readium.r2.shared.util.format.Format -import org.readium.r2.shared.util.format.FormatRegistry -import org.readium.r2.shared.util.http.HttpClient -import org.readium.r2.shared.util.http.HttpContainer -import org.readium.r2.shared.util.resource.SingleResourceContainer -import org.readium.r2.streamer.parser.PublicationParser -import timber.log.Timber - -internal class ParserAssetFactory( - private val httpClient: HttpClient, - private val formatRegistry: FormatRegistry -) { - - sealed class CreateError( - override val message: String, - override val cause: Error? - ) : Error { - - class Reading( - override val cause: ReadError - ) : CreateError("An error occurred while trying to read asset.", cause) - - class FormatNotSupported( - override val cause: Error? - ) : CreateError("Asset is not supported.", cause) - } - - suspend fun createParserAsset( - asset: Asset - ): Try { - return when (asset) { - is ContainerAsset -> - createParserAssetForContainer(asset) - is ResourceAsset -> - createParserAssetForResource(asset) - } - } - - private fun createParserAssetForContainer( - asset: ContainerAsset - ): Try = - Try.success( - PublicationParser.Asset( - format = asset.format, - container = asset.container - ) - ) - - private suspend fun createParserAssetForResource( - asset: ResourceAsset - ): Try = - if (asset.format.conformsTo(Format.RWPM)) { - createParserAssetForManifest(asset) - } else { - createParserAssetForContent(asset) - } - - private suspend fun createParserAssetForManifest( - asset: ResourceAsset - ): Try { - val manifest = asset.resource - .readDecodeOrElse( - decode = { it.decodeRwpm() }, - recover = { return Try.failure(CreateError.Reading(it)) } - ) - - val baseUrl = manifest.linkWithRel("self")?.href?.resolve() - if (baseUrl == null) { - Timber.w("No self link found in the manifest at ${asset.resource.sourceUrl}") - } else { - if (baseUrl !is AbsoluteUrl) { - return Try.failure( - CreateError.Reading( - ReadError.Decoding("Self link is not absolute.") - ) - ) - } - if (!baseUrl.isHttp) { - return Try.failure( - CreateError.FormatNotSupported( - DebugError("Self link doesn't use the HTTP(S) scheme.") - ) - ) - } - } - - val resources = (manifest.readingOrder + manifest.resources) - .map { it.url() } - .toSet() - - val container = - CompositeContainer( - SingleResourceContainer( - Url("manifest.json")!!, - asset.resource - ), - HttpContainer(baseUrl, resources, httpClient) - ) - - return Try.success( - PublicationParser.Asset( - format = Format.RPF, - container = container - ) - ) - } - - private fun createParserAssetForContent( - asset: ResourceAsset - ): Try { - // Historically, the reading order of a standalone file contained a single link with the - // HREF "/$assetName". This was fragile if the asset named changed, or was different on - // other devices. To avoid this, we now use a single link with the HREF - // "publication.extension". - val extension = formatRegistry[asset.format] - ?.fileExtension?.value?.addPrefix(".") - ?: "" - val container = SingleResourceContainer( - Url("publication$extension")!!, - asset.resource - ) - - return Try.success( - PublicationParser.Asset( - format = asset.format, - container = container - ) - ) - } -} From 579e60e515a790fd6c40eac207773ff77101e9bb Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 13 Dec 2023 16:07:15 +0100 Subject: [PATCH 77/86] Remove ParserAssetFactory --- .../readium/r2/lcp/LcpContentProtection.kt | 16 +++-- .../AdeptFallbackContentProtection.kt | 7 +- .../protection/ContentProtection.kt | 35 +++++----- .../LcpFallbackContentProtection.kt | 7 +- .../readium/r2/streamer/PublicationFactory.kt | 54 +++------------ .../r2/streamer/extensions/Container.kt | 22 ++++++ .../r2/streamer/parser/PublicationParser.kt | 17 +---- .../r2/streamer/parser/audio/AudioParser.kt | 23 +++++-- .../r2/streamer/parser/epub/EpubParser.kt | 6 +- .../r2/streamer/parser/image/ImageParser.kt | 23 +++++-- .../r2/streamer/parser/pdf/PdfParser.kt | 30 ++++----- .../parser/readium/ReadiumWebPubParser.kt | 67 ++++++++++++++++++- .../java/org/readium/r2/streamer/TestUtils.kt | 3 +- .../streamer/parser/image/ImageParserTest.kt | 8 +-- 14 files changed, 187 insertions(+), 131 deletions(-) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index e3759f06b4..395194a516 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -39,7 +39,7 @@ internal class LcpContentProtection( asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { if ( !asset.format.conformsTo(Format.EPUB_LCP) && !asset.format.conformsTo(Format.RPF_LCP) && @@ -61,7 +61,7 @@ internal class LcpContentProtection( asset: ContainerAsset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { val license = retrieveLicense(asset, credentials, allowUserInteraction) return createResultAsset(asset, license) } @@ -81,7 +81,7 @@ internal class LcpContentProtection( private fun createResultAsset( asset: ContainerAsset, license: Try - ): Try { + ): Try { val serviceFactory = LcpContentProtectionService .createFactory(license.getOrNull(), license.failureOrNull()) @@ -89,9 +89,11 @@ internal class LcpContentProtection( val container = TransformingContainer(asset.container, decryptor::transform) - val protectedFile = ContentProtection.Asset( - format = asset.format, - container = container, + val protectedFile = ContentProtection.OpenResult( + asset = ContainerAsset( + format = asset.format, + container = container + ), onCreatePublication = { decryptor.encryptionData = (manifest.readingOrder + manifest.resources + manifest.links) .flatten() @@ -111,7 +113,7 @@ internal class LcpContentProtection( licenseAsset: ResourceAsset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction) val licenseDoc = license.getOrNull()?.license diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt index 2034c8ed0b..11dd13c3c1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt @@ -27,14 +27,13 @@ public class AdeptFallbackContentProtection : ContentProtection { asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { if (asset !is ContainerAsset || !asset.format.conformsTo(Format.EPUB_ADEPT)) { return Try.failure(ContentProtection.OpenError.AssetNotSupported()) } - val protectedFile = ContentProtection.Asset( - asset.format, - asset.container, + val protectedFile = ContentProtection.OpenResult( + asset = asset, onCreatePublication = { servicesBuilder.contentProtectionServiceFactory = FallbackContentProtectionService.createFactory(scheme, "Adobe ADEPT") diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index 6c63ec064e..6c6575ddb1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -14,10 +14,9 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.services.ContentProtectionService import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.format.Format -import org.readium.r2.shared.util.resource.Resource /** * Bridge between a Content Protection technology and the Readium toolkit. @@ -42,6 +41,19 @@ public interface ContentProtection { ) : OpenError("Asset is not supported.", cause) } + /** + * Holds the result of opening an [OpenResult] with a [ContentProtection]. + * + * @property asset Asset pointing to a publication. + * @property onCreatePublication Called on every parsed Publication.Builder + * It can be used to modify the `Manifest`, the root [Container] or the list of service + * factories of a [Publication]. + */ + public data class OpenResult( + val asset: Asset, + val onCreatePublication: Publication.Builder.() -> Unit = {} + ) + public val scheme: Scheme /** @@ -51,25 +63,10 @@ public interface ContentProtection { * asset can't be successfully opened even in restricted mode. */ public suspend fun open( - asset: org.readium.r2.shared.util.asset.Asset, + asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try - - /** - * Holds the result of opening an [Asset] with a [ContentProtection]. - * - * @property format Format of the asset - * @property container Container to access the publication through - * @property onCreatePublication Called on every parsed Publication.Builder - * It can be used to modify the `Manifest`, the root [Container] or the list of service - * factories of a [Publication]. - */ - public data class Asset( - val format: Format, - val container: Container, - val onCreatePublication: Publication.Builder.() -> Unit = {} - ) + ): Try /** * Represents a specific Content Protection technology, uniquely identified with an [uri]. diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index 45a1c3b2cc..67e3c31716 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -28,7 +28,7 @@ public class LcpFallbackContentProtection : ContentProtection { asset: Asset, credentials: String?, allowUserInteraction: Boolean - ): Try { + ): Try { if ( !asset.format.conformsTo(Format.EPUB_LCP) && !asset.format.conformsTo(Format.RPF_LCP) && @@ -45,9 +45,8 @@ public class LcpFallbackContentProtection : ContentProtection { ) } - val protectedFile = ContentProtection.Asset( - asset.format, - asset.container, + val protectedFile = ContentProtection.OpenResult( + asset = asset, onCreatePublication = { servicesBuilder.contentProtectionServiceFactory = FallbackContentProtectionService.createFactory(scheme, "Readium LCP") diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index a4d8ad4d76..4e3cd4bbcd 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -17,7 +17,7 @@ import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetSniffer -import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpClient @@ -65,7 +65,7 @@ public class PublicationFactory( ) : Error { public class Reading( - override val cause: org.readium.r2.shared.util.data.ReadError + override val cause: ReadError ) : OpenError("An error occurred while trying to read asset.", cause) public class FormatNotSupported( @@ -92,9 +92,6 @@ public class PublicationFactory( private val parsers: List = parsers + if (!ignoreDefaultParsers) defaultParsers else emptyList() - private val parserAssetFactory: ParserAssetFactory = - ParserAssetFactory(httpClient, formatRegistry) - /** * Opens a [Publication] from the given asset. * @@ -125,21 +122,12 @@ public class PublicationFactory( onCreatePublication: Publication.Builder.() -> Unit = {}, warnings: WarningLogger? = null ): Try { - val compositeOnCreatePublication: Publication.Builder.() -> Unit = { + var compositeOnCreatePublication: Publication.Builder.() -> Unit = { this@PublicationFactory.onCreatePublication(this) onCreatePublication(this) } - parserAssetFactory.createParserAsset(asset) - .getOrElse { - when (it) { - is ParserAssetFactory.CreateError.Reading -> - return Try.failure(OpenError.Reading(it.cause)) - is ParserAssetFactory.CreateError.FormatNotSupported -> - null - } - } - ?.let { openParserAsset(it, compositeOnCreatePublication, warnings) } + var transformedAsset: Asset = asset for (protection in contentProtections) { protection.open(asset, credentials, allowUserInteraction) @@ -150,36 +138,16 @@ public class PublicationFactory( is ContentProtection.OpenError.AssetNotSupported -> null } - }?.let { protectedAsset -> - val parserAsset = PublicationParser.Asset( - protectedAsset.format, - protectedAsset.container - ) - - val fullOnCreatePublication: Publication.Builder.() -> Unit = { - protectedAsset.onCreatePublication.invoke(this) - onCreatePublication(this) + }?.let { openResult -> + transformedAsset = openResult.asset + compositeOnCreatePublication = { + openResult.onCreatePublication.invoke(this) + compositeOnCreatePublication(this) } - - return openParserAsset(parserAsset, fullOnCreatePublication) } } - if (asset !is ContainerAsset) { - return Try.failure(OpenError.FormatNotSupported()) - } - - val parserAsset = PublicationParser.Asset(asset.format, asset.container) - - return openParserAsset(parserAsset, compositeOnCreatePublication, warnings) - } - - private suspend fun openParserAsset( - publicationAsset: PublicationParser.Asset, - onCreatePublication: Publication.Builder.() -> Unit = {}, - warnings: WarningLogger? = null - ): Try { - val builder = parse(publicationAsset, warnings) + val builder = parse(transformedAsset, warnings) .getOrElse { return Try.failure(wrapParserException(it)) } builder.apply(onCreatePublication) @@ -189,7 +157,7 @@ public class PublicationFactory( } private suspend fun parse( - publicationAsset: PublicationParser.Asset, + publicationAsset: Asset, warnings: WarningLogger? ): Try { for (parser in parsers) { diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt index 00cdacf034..7887afba3a 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt @@ -10,7 +10,12 @@ package org.readium.r2.streamer.extensions import java.io.File +import org.readium.r2.shared.extensions.addPrefix import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.format.FileExtension +import org.readium.r2.shared.util.resource.Resource +import org.readium.r2.shared.util.resource.SingleResourceContainer internal fun Iterable.guessTitle(): String? { val firstEntry = firstOrNull() ?: return null @@ -30,3 +35,20 @@ internal fun Iterable.pathCommonFirstComponent(): File? = .takeIf { it.size == 1 } ?.firstOrNull() ?.let { File(it) } + +internal fun Resource.toContainer( + entryExtension: FileExtension? = sourceUrl?.extension?.let { FileExtension((it)) } +): Container { + // Historically, the reading order of a standalone file contained a single link with the + // HREF "/$assetName". This was fragile if the asset named changed, or was different on + // other devices. To avoid this, we now use a single link with the HREF + // "publication.extension". + val extension = entryExtension?.value + ?.addPrefix(".") + ?: "" + + return SingleResourceContainer( + Url("publication$extension")!!, + this + ) +} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt index c7bb47c021..b18b2123c1 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt @@ -8,29 +8,14 @@ package org.readium.r2.streamer.parser import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.Container -import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.logging.WarningLogger -import org.readium.r2.shared.util.resource.Resource /** * Parses a Publication from an asset. */ public interface PublicationParser { - /** - * Full publication asset. - * - * @param format Format of the "virtual" publication asset, built from the source asset. - * For example, if the source asset media type was a `application/audiobook+json`, the "virtual" asset - * media type will be `application/audiobook+zip`. - * @param container Container granting access to the resources of the publication. - */ - public data class Asset( - val format: Format, - val container: Container - ) - /** * Constructs a [Publication.Builder] to build a [Publication] from a publication asset. * diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index 2a7f1901ff..ff82ccfb36 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -14,7 +14,10 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetSniffer +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.format.Format @@ -24,6 +27,7 @@ import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs +import org.readium.r2.streamer.extensions.toContainer import org.readium.r2.streamer.parser.PublicationParser /** @@ -38,21 +42,28 @@ public class AudioParser( ) : PublicationParser { override suspend fun parse( - asset: PublicationParser.Asset, + asset: Asset, warnings: WarningLogger? ): Try { if (!asset.format.conformsTo(Format.ZAB) && formatRegistry[asset.format]?.mediaType?.isAudio != true) { return Try.failure(PublicationParser.Error.FormatNotSupported()) } + val container = when (asset) { + is ResourceAsset -> + asset.resource.toContainer() + is ContainerAsset -> + asset.container + } + val readingOrder = if (asset.format.conformsTo(Format.ZAB)) { - asset.container + container .filter { zabCanContain(it) } .sortedBy { it.toString() } } else { listOfNotNull( - asset.container.entries.firstOrNull() + container.entries.firstOrNull() ) } @@ -67,7 +78,7 @@ public class AudioParser( } val readingOrderLinks = readingOrder.map { url -> - val mediaType = asset.container[url]!!.use { resource -> + val mediaType = container[url]!!.use { resource -> assetSniffer.sniff(resource) .map { formatRegistry[it]?.mediaType } .getOrElse { error -> @@ -85,14 +96,14 @@ public class AudioParser( val manifest = Manifest( metadata = Metadata( conformsTo = setOf(Publication.Profile.AUDIOBOOK), - localizedTitle = asset.container.entries.guessTitle()?.let { LocalizedString(it) } + localizedTitle = container.entries.guessTitle()?.let { LocalizedString(it) } ), readingOrder = readingOrderLinks ) val publicationBuilder = Publication.Builder( manifest = manifest, - container = asset.container, + container = container, servicesBuilder = Publication.ServicesBuilder( locator = AudioLocatorService.createFactory() ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index c0a4939267..d214879607 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -17,6 +17,8 @@ import org.readium.r2.shared.publication.services.search.StringSearchService import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.DecodeError import org.readium.r2.shared.util.data.ReadError @@ -47,10 +49,10 @@ public class EpubParser( ) : PublicationParser { override suspend fun parse( - asset: PublicationParser.Asset, + asset: Asset, warnings: WarningLogger? ): Try { - if (!asset.format.conformsTo(Format.EPUB)) { + if (asset !is ContainerAsset || !asset.format.conformsTo(Format.EPUB)) { return Try.failure(PublicationParser.Error.FormatNotSupported()) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index 7e84e3f1a6..2fc34f44b2 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -15,7 +15,10 @@ import org.readium.r2.shared.publication.services.PerResourcePositionsService import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetSniffer +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.format.Format @@ -26,6 +29,7 @@ import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs +import org.readium.r2.streamer.extensions.toContainer import org.readium.r2.streamer.parser.PublicationParser /** @@ -40,20 +44,27 @@ public class ImageParser( ) : PublicationParser { override suspend fun parse( - asset: PublicationParser.Asset, + asset: Asset, warnings: WarningLogger? ): Try { if (!asset.format.conformsTo(Format.CBZ) && formatRegistry[asset.format]?.mediaType?.isBitmap != true) { return Try.failure(PublicationParser.Error.FormatNotSupported()) } + val container = when (asset) { + is ResourceAsset -> + asset.resource.toContainer() + is ContainerAsset -> + asset.container + } + val readingOrder = if (asset.format.conformsTo(Format.CBZ)) { - (asset.container) + (container) .filter { cbzCanContain(it) } .sortedBy { it.toString() } } else { - listOfNotNull(asset.container.firstOrNull()) + listOfNotNull(container.firstOrNull()) } if (readingOrder.isEmpty()) { @@ -67,7 +78,7 @@ public class ImageParser( } val readingOrderLinks = readingOrder.map { url -> - val mediaType = asset.container[url]!!.use { resource -> + val mediaType = container[url]!!.use { resource -> assetSniffer.sniff(resource) .map { formatRegistry[it]?.mediaType } .getOrElse { error -> @@ -88,14 +99,14 @@ public class ImageParser( val manifest = Manifest( metadata = Metadata( conformsTo = setOf(Publication.Profile.DIVINA), - localizedTitle = asset.container.guessTitle()?.let { LocalizedString(it) } + localizedTitle = container.guessTitle()?.let { LocalizedString(it) } ), readingOrder = readingOrderLinks ) val publicationBuilder = Publication.Builder( manifest = manifest, - container = asset.container, + container = container, servicesBuilder = Publication.ServicesBuilder( positions = PerResourcePositionsService.createFactory( fallbackMediaType = MediaType("image/*")!! diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index 9243e5a04e..0d5070136e 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -12,15 +12,17 @@ import org.readium.r2.shared.PdfSupport import org.readium.r2.shared.publication.* import org.readium.r2.shared.publication.services.InMemoryCacheService import org.readium.r2.shared.publication.services.InMemoryCoverService -import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.pdf.PdfDocumentFactory import org.readium.r2.shared.util.pdf.toLinks +import org.readium.r2.streamer.extensions.toContainer import org.readium.r2.streamer.parser.PublicationParser /** @@ -36,26 +38,20 @@ public class PdfParser( private val context = context.applicationContext override suspend fun parse( - asset: PublicationParser.Asset, + asset: Asset, warnings: WarningLogger? ): Try { - if (!asset.format.conformsTo(Format.PDF)) { + if (asset !is ResourceAsset || !asset.format.conformsTo(Format.PDF)) { return Try.failure(PublicationParser.Error.FormatNotSupported()) } - val url = asset.container.entries - .firstOrNull() + val container = asset.resource + .toContainer(FormatRegistry()[Format.PDF]?.fileExtension) - val resource = url - ?.let { asset.container[it] } - ?: return Try.failure( - PublicationParser.Error.Reading( - ReadError.Decoding( - DebugError("No PDF found in the publication.") - ) - ) - ) - val document = pdfFactory.open(resource, password = null) + val url = container.entries + .first() + + val document = pdfFactory.open(container[url]!!, password = null) .getOrElse { return Try.failure(PublicationParser.Error.Reading(it)) } val tableOfContents = document.outline.toLinks(url) @@ -78,7 +74,7 @@ public class PdfParser( cover = document.cover(context)?.let { InMemoryCoverService.createFactory(it) } ) - val publicationBuilder = Publication.Builder(manifest, asset.container, servicesBuilder) + val publicationBuilder = Publication.Builder(manifest, container, servicesBuilder) return Try.success(publicationBuilder) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index 8f3c384620..30472522c0 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -14,19 +14,28 @@ import org.readium.r2.shared.publication.services.WebPositionsService import org.readium.r2.shared.publication.services.cacheServiceFactory import org.readium.r2.shared.publication.services.locatorServiceFactory import org.readium.r2.shared.publication.services.positionsServiceFactory +import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.Asset +import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset +import org.readium.r2.shared.util.data.CompositeContainer import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.decodeRwpm import org.readium.r2.shared.util.data.readDecodeOrElse import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpContainer import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.pdf.PdfDocumentFactory +import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.streamer.parser.PublicationParser import org.readium.r2.streamer.parser.audio.AudioLocatorService +import timber.log.Timber /** * Parses any Readium Web Publication package or manifest, e.g. WebPub, Audiobook, DiViNa, LCPDF... @@ -38,10 +47,16 @@ public class ReadiumWebPubParser( ) : PublicationParser { override suspend fun parse( - asset: PublicationParser.Asset, + asset: Asset, warnings: WarningLogger? ): Try { - if (!asset.format.conformsTo(Format.RPF)) { + if (asset is ResourceAsset && asset.format.conformsTo(Format.RWPM)) { + val packageAsset = createPackage(asset) + .getOrElse { return Try.failure(it) } + return parse(packageAsset, warnings) + } + + if (asset !is ContainerAsset || !asset.format.conformsTo(Format.RPF)) { return Try.failure(PublicationParser.Error.FormatNotSupported()) } @@ -96,4 +111,52 @@ public class ReadiumWebPubParser( val publicationBuilder = Publication.Builder(manifest, asset.container, servicesBuilder) return Try.success(publicationBuilder) } + + private suspend fun createPackage(asset: ResourceAsset): Try { + val manifest = asset.resource + .readDecodeOrElse( + decode = { it.decodeRwpm() }, + recover = { return Try.failure(PublicationParser.Error.Reading(it)) } + ) + + val baseUrl = manifest.linkWithRel("self")?.href?.resolve() + if (baseUrl == null) { + Timber.w("No self link found in the manifest at ${asset.resource.sourceUrl}") + } else { + if (baseUrl !is AbsoluteUrl) { + return Try.failure( + PublicationParser.Error.Reading( + ReadError.Decoding("Self link is not absolute.") + ) + ) + } + if (!baseUrl.isHttp) { + return Try.failure( + PublicationParser.Error.Reading( + ReadError.Decoding("Self link doesn't use the HTTP(S) scheme.") + ) + ) + } + } + + val resources = (manifest.readingOrder + manifest.resources) + .map { it.url() } + .toSet() + + val container = + CompositeContainer( + SingleResourceContainer( + Url("manifest.json")!!, + asset.resource + ), + HttpContainer(baseUrl, resources, httpClient) + ) + + return Try.success( + ContainerAsset( + format = Format.RPF, + container = container + ) + ) + } } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/TestUtils.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/TestUtils.kt index 6b18b4dc28..535863857c 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/TestUtils.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/TestUtils.kt @@ -11,10 +11,11 @@ package org.readium.r2.streamer import kotlinx.coroutines.runBlocking import org.readium.r2.shared.publication.Publication +import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.resource.Resource import org.readium.r2.streamer.parser.PublicationParser internal fun Resource.readBlocking(range: LongRange? = null) = runBlocking { read(range) } -internal fun PublicationParser.parseBlocking(asset: PublicationParser.Asset): +internal fun PublicationParser.parseBlocking(asset: Asset): Publication.Builder? = runBlocking { parse(asset).getOrNull() } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index 4a96be9016..f60dd07b5a 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -20,6 +20,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.firstWithRel import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.AssetSniffer +import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.format.Format @@ -29,7 +30,6 @@ import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.zip.ZipArchiveOpener import org.readium.r2.streamer.parseBlocking -import org.readium.r2.streamer.parser.PublicationParser import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -47,14 +47,14 @@ class ImageParserTest { val file = fileForResource("futuristic_tales.cbz") val resource = FileResource(file) val archive = archiveOpener.open(Format.ZIP, resource).checkSuccess() - PublicationParser.Asset(format = Format.CBZ, archive) + ContainerAsset(Format.CBZ, archive) } private val jpgAsset = runBlocking { val file = fileForResource("futuristic_tales.jpg") val resource = FileResource(file, mediaType = MediaType.JPEG) - PublicationParser.Asset( - format = Format.JPEG, + ContainerAsset( + Format.JPEG, SingleResourceContainer(file.toUrl(), resource) ) } From 7ac223e5c3cbe22852001caf3e7e7a97b2459bcc Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Sun, 17 Dec 2023 19:18:00 +0100 Subject: [PATCH 78/86] Fix tests --- .../readium/r2/lcp/LcpContentProtection.kt | 7 +- .../AdeptFallbackContentProtection.kt | 4 +- .../LcpFallbackContentProtection.kt | 8 +- .../r2/shared/util/asset/AssetSniffer.kt | 2 - .../r2/shared/util/format/DefaultSniffers.kt | 217 ++++++++---------- .../readium/r2/shared/util/format/Format.kt | 202 +++++++++++----- .../r2/shared/util/format/FormatRegistry.kt | 41 +++- .../shared/util/zip/FileZipArchiveProvider.kt | 3 +- .../util/zip/StreamingZipArchiveProvider.kt | 3 +- .../r2/shared/util/zip/ZipArchiveOpener.kt | 4 +- .../shared/util/format/DefaultSniffersTest.kt | 12 +- .../shared/util/mediatype/AssetSnifferTest.kt | 71 +++--- .../parser/readium/ReadiumWebPubParser.kt | 38 ++- .../org/readium/r2/testapp/data/model/Book.kt | 2 +- 14 files changed, 355 insertions(+), 259 deletions(-) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index 395194a516..9a5f948968 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -23,6 +23,7 @@ import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.Trait import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.TransformingContainer @@ -41,11 +42,7 @@ internal class LcpContentProtection( allowUserInteraction: Boolean ): Try { if ( - !asset.format.conformsTo(Format.EPUB_LCP) && - !asset.format.conformsTo(Format.RPF_LCP) && - !asset.format.conformsTo(Format.RPF_AUDIO_LCP) && - !asset.format.conformsTo(Format.RPF_IMAGE_LCP) && - !asset.format.conformsTo(Format.RPF_PDF_LCP) && + !asset.format.conformsTo(Trait.LCP_PROTECTED) && !asset.format.conformsTo(Format.LCP_LICENSE_DOCUMENT) ) { return Try.failure(ContentProtection.OpenError.AssetNotSupported()) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt index 11dd13c3c1..96282568cb 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt @@ -12,7 +12,7 @@ import org.readium.r2.shared.publication.services.contentProtectionServiceFactor import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.ContainerAsset -import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.Trait /** * [ContentProtection] implementation used as a fallback by the Streamer to detect Adept DRM, @@ -28,7 +28,7 @@ public class AdeptFallbackContentProtection : ContentProtection { credentials: String?, allowUserInteraction: Boolean ): Try { - if (asset !is ContainerAsset || !asset.format.conformsTo(Format.EPUB_ADEPT)) { + if (asset !is ContainerAsset || !asset.format.conformsTo(Trait.ADEPT_PROTECTED)) { return Try.failure(ContentProtection.OpenError.AssetNotSupported()) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt index 67e3c31716..056674dcb3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt @@ -12,7 +12,7 @@ import org.readium.r2.shared.publication.services.contentProtectionServiceFactor import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.ContainerAsset -import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.Trait /** * [ContentProtection] implementation used as a fallback by the Streamer to detect LCP DRM @@ -30,11 +30,7 @@ public class LcpFallbackContentProtection : ContentProtection { allowUserInteraction: Boolean ): Try { if ( - !asset.format.conformsTo(Format.EPUB_LCP) && - !asset.format.conformsTo(Format.RPF_LCP) && - !asset.format.conformsTo(Format.RPF_AUDIO_LCP) && - !asset.format.conformsTo(Format.RPF_IMAGE_LCP) && - !asset.format.conformsTo(Format.RPF_PDF_LCP) + !asset.format.conformsTo(Trait.LCP_PROTECTED) ) { return Try.failure(ContentProtection.OpenError.AssetNotSupported()) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt index 78a14d76b8..f9394a0c5a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt @@ -34,7 +34,6 @@ import org.readium.r2.shared.util.format.RarSniffer import org.readium.r2.shared.util.format.RpfSniffer import org.readium.r2.shared.util.format.RwpmSniffer import org.readium.r2.shared.util.format.W3cWpubSniffer -import org.readium.r2.shared.util.format.XhtmlSniffer import org.readium.r2.shared.util.format.ZipSniffer import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.Resource @@ -59,7 +58,6 @@ public class AssetSniffer( ArchiveSniffer, RpfSniffer, PdfSniffer, - XhtmlSniffer, HtmlSniffer, BitmapSniffer, JsonSniffer, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt index 4b51e8ff94..d64be5e09b 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt @@ -26,53 +26,11 @@ import org.readium.r2.shared.util.data.decodeString import org.readium.r2.shared.util.data.decodeXml import org.readium.r2.shared.util.data.readDecodeOrElse import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.format.Format.Companion.orEmpty import org.readium.r2.shared.util.getOrDefault import org.readium.r2.shared.util.getOrElse -/** - * Sniffs an XHTML document. - * - * Must precede the HTML sniffer. - */ -public object XhtmlSniffer : ContentSniffer { - override fun sniffHints( - format: Format?, - hints: FormatHints - ): Format? { - if ( - hints.hasFileExtension("xht", "xhtml") || - hints.hasMediaType("application/xhtml+xml") - ) { - return Format.XHTML - } - - return format - } - - override suspend fun sniffBlob( - format: Format?, - source: Readable - ): Try { - if (format != null && format != Format.XML || !source.canReadWholeBlob()) { - return Try.success(format) - } - - source.readDecodeOrElse( - decode = { it.decodeXml() }, - recoverRead = { return Try.failure(it) }, - recoverDecode = { null } - )?.takeIf { - it.name.lowercase(Locale.ROOT) == "html" && - it.namespace.lowercase(Locale.ROOT).contains("xhtml") - }?.let { - return Try.success(Format.XHTML) - } - - return Try.success(format) - } -} - -/** Sniffs an HTML document. */ +/** Sniffs an HTML or XHTML document. */ public object HtmlSniffer : ContentSniffer { override fun sniffHints( format: Format?, @@ -85,6 +43,13 @@ public object HtmlSniffer : ContentSniffer { return Format.HTML } + if ( + hints.hasFileExtension("xht", "xhtml") || + hints.hasMediaType("application/xhtml+xml") + ) { + return Format.XHTML + } + return format } @@ -92,7 +57,7 @@ public object HtmlSniffer : ContentSniffer { format: Format?, source: Readable ): Try { - if (format != null || !source.canReadWholeBlob()) { + if (format != null && format != Format.XML || !source.canReadWholeBlob()) { return Try.success(format) } @@ -103,7 +68,15 @@ public object HtmlSniffer : ContentSniffer { recoverDecode = { null } ) ?.takeIf { it.name.lowercase(Locale.ROOT) == "html" } - ?.let { return Try.success(Format.HTML) } + ?.let { + return Try.success( + if (it.namespace.lowercase(Locale.ROOT).contains("xhtml")) { + Format.XHTML + } else { + Format.HTML + } + ) + } source.readDecodeOrElse( decode = { it.decodeString() }, @@ -140,7 +113,7 @@ public object OpdsSniffer : ContentSniffer { return Format.OPDS1_ACQUISITION_FEED } if (hints.hasMediaType("application/atom+xml;profile=opds-catalog")) { - return Format.OPDS1 + return Format.OPDS1_CATALOG } return format @@ -149,7 +122,7 @@ public object OpdsSniffer : ContentSniffer { private fun sniffHintsJson(format: Format?, hints: FormatHints): Format? { // OPDS 2 if (hints.hasMediaType("application/opds+json")) { - return Format.OPDS2 + return Format.OPDS2_CATALOG } if (hints.hasMediaType("application/opds-publication+json")) { return Format.OPDS2_PUBLICATION @@ -195,7 +168,7 @@ public object OpdsSniffer : ContentSniffer { )?.takeIf { it.namespace == "http://www.w3.org/2005/Atom" } ?.let { xml -> if (xml.name == "feed") { - return Try.success(Format.OPDS1) + return Try.success(Format.OPDS1_CATALOG) } else if (xml.name == "entry") { return Try.success(Format.OPDS1_ENTRY) } @@ -205,7 +178,7 @@ public object OpdsSniffer : ContentSniffer { } private suspend fun sniffBlobJson(format: Format?, source: Readable): Try { - if (format !in listOf(null, Format.JSON, Format.RWPM)) { + if (format != null && format != Format.JSON) { return Try.success(format) } @@ -218,7 +191,7 @@ public object OpdsSniffer : ContentSniffer { ?.let { rwpm -> if (rwpm.linkWithRel("self")?.mediaType?.matches("application/opds+json") == true ) { - return Try.success(Format.OPDS2) + return Try.success(Format.OPDS2_CATALOG) } /** @@ -293,49 +266,49 @@ public object BitmapSniffer : ContentSniffer { hints.hasFileExtension("avif") || hints.hasMediaType("image/avif") ) { - return Format.AVIF + return Format(setOf(Trait.BITMAP, Trait.AVIF)) } if ( hints.hasFileExtension("bmp", "dib") || hints.hasMediaType("image/bmp", "image/x-bmp") ) { - return Format.BMP + return Format(setOf(Trait.BITMAP, Trait.BMP)) } if ( hints.hasFileExtension("gif") || hints.hasMediaType("image/gif") ) { - return Format.GIF + return Format(setOf(Trait.BITMAP, Trait.GIF)) } if ( hints.hasFileExtension("jpg", "jpeg", "jpe", "jif", "jfif", "jfi") || hints.hasMediaType("image/jpeg") ) { - return Format.JPEG + return Format(setOf(Trait.BITMAP, Trait.JPEG)) } if ( hints.hasFileExtension("jxl") || hints.hasMediaType("image/jxl") ) { - return Format.JXL + return Format(setOf(Trait.BITMAP, Trait.JXL)) } if ( hints.hasFileExtension("png") || hints.hasMediaType("image/png") ) { - return Format.PNG + return Format(setOf(Trait.BITMAP, Trait.PNG)) } if ( hints.hasFileExtension("tiff", "tif") || hints.hasMediaType("image/tiff", "image/tiff-fx") ) { - return Format.TIFF + return Format(setOf(Trait.BITMAP, Trait.TIFF)) } if ( hints.hasFileExtension("webp") || hints.hasMediaType("image/webp") ) { - return Format.WEBP + return Format(setOf(Trait.BITMAP, Trait.WEBP)) } return format } @@ -348,15 +321,15 @@ public object RwpmSniffer : ContentSniffer { hints: FormatHints ): Format? { if (hints.hasMediaType("application/audiobook+json")) { - return Format.RWPM_AUDIO + return Format.READIUM_AUDIOBOOK_MANIFEST } if (hints.hasMediaType("application/divina+json")) { - return Format.RWPM_IMAGE + return Format.READIUM_COMICS_MANIFEST } if (hints.hasMediaType("application/webpub+json")) { - return Format.RWPM + return Format.READIUM_WEBPUB_MANIFEST } return format @@ -381,14 +354,14 @@ public object RwpmSniffer : ContentSniffer { ) ?: return Try.success(format) if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { - return Try.success(Format.RWPM_AUDIO) + return Try.success(Format.READIUM_AUDIOBOOK_MANIFEST) } if (manifest.conformsTo(Publication.Profile.DIVINA)) { - return Try.success(Format.RWPM_IMAGE) + return Try.success(Format.READIUM_COMICS_MANIFEST) } if (manifest.linkWithRel("self")?.mediaType?.matches("application/webpub+json") == true) { - return Try.success(Format.RWPM) + return Try.success(Format.READIUM_WEBPUB_MANIFEST) } return Try.success(format) @@ -406,34 +379,34 @@ public object RpfSniffer : ContentSniffer { hints.hasFileExtension("audiobook") || hints.hasMediaType("application/audiobook+zip") ) { - return Format.RPF_AUDIO + return Format.READIUM_AUDIOBOOK } if ( hints.hasFileExtension("divina") || hints.hasMediaType("application/divina+zip") ) { - return Format.RPF_IMAGE + return Format.READIUM_COMICS } if ( hints.hasFileExtension("webpub") || hints.hasMediaType("application/webpub+zip") ) { - return Format.RPF + return Format.READIUM_WEBPUB } if ( hints.hasFileExtension("lcpa") || hints.hasMediaType("application/audiobook+lcp") ) { - return Format.RPF_AUDIO_LCP + return Format.READIUM_AUDIOBOOK + Trait.LCP_PROTECTED } if ( hints.hasFileExtension("lcpdf") || hints.hasMediaType("application/pdf+lcp") ) { - return Format.RPF_PDF_LCP + return Format.READIUM_PDF + Trait.LCP_PROTECTED } return format @@ -443,14 +416,8 @@ public object RpfSniffer : ContentSniffer { format: Format?, container: Container ): Try { - // Recognize exploded RPF. if ( - format != null && format != Format.ZIP && format != Format.RPF || - format in listOf( - Format.RPF_AUDIO, - Format.RPF_IMAGE, - Format.RPF_PDF - ) + format != null && format != Format.ZIP ) { return Try.success(format) } @@ -464,16 +431,16 @@ public object RpfSniffer : ContentSniffer { ?: return Try.success(format) if (manifest.conformsTo(Publication.Profile.AUDIOBOOK)) { - return Try.success(Format.RPF_AUDIO) + return Try.success(Format.READIUM_AUDIOBOOK) } if (manifest.conformsTo(Publication.Profile.DIVINA)) { - return Try.success(Format.RPF_IMAGE) + return Try.success(Format.READIUM_COMICS) } if (manifest.conformsTo(Publication.Profile.PDF)) { - return Try.success(Format.RPF_PDF) + return Try.success(Format.READIUM_PDF) } - return Try.success(Format.RPF) + return Try.success(Format.READIUM_WEBPUB) } } @@ -498,7 +465,13 @@ public object W3cWpubSniffer : ContentSniffer { string.contains("@context") && string.contains("https://www.w3.org/ns/wp-context") ) { - return Try.success(Format.W3C_WPUB_MANIFEST) + return Try.success( + if (string.contains("https://www.w3.org/TR/audiobooks/")) { + Format(setOf(Trait.JSON, Trait.W3C_AUDIOBOOK_MANIFEST)) + } else { + Format(setOf(Trait.JSON, Trait.W3C_PUB_MANIFEST)) + } + ) } return Try.success(format) @@ -529,8 +502,7 @@ public object EpubSniffer : ContentSniffer { format: Format?, container: Container ): Try { - // Recognize exploded EPUBs. - if (format != null && format != Format.ZIP || format == Format.EPUB) { + if (format != null && format != Format.ZIP) { return Try.success(format) } @@ -542,7 +514,7 @@ public object EpubSniffer : ContentSniffer { )?.trim() if (mimetype == "application/epub+zip") { - return Try.success(Format.EPUB) + return Try.success(format.orEmpty() + Trait.EPUB) } return Try.success(format) @@ -565,7 +537,7 @@ public object LpfSniffer : ContentSniffer { hints.hasFileExtension("lpf") || hints.hasMediaType("application/lpf+zip") ) { - return Format.LPF + return Format(setOf(Trait.ZIP, Trait.LPF)) } return format @@ -575,13 +547,12 @@ public object LpfSniffer : ContentSniffer { format: Format?, container: Container ): Try { - // Recognize exploded LPFs. - if (format != null && format != Format.ZIP || format == Format.LPF) { + if (format != null && format != Format.ZIP) { return Try.success(format) } if (RelativeUrl("index.html")!! in container) { - return Try.success(Format.LPF) + return Try.success(format.orEmpty() + Trait.LPF) } // Somehow, [JSONObject] can't access JSON-LD keys such as `@content`. @@ -594,7 +565,13 @@ public object LpfSniffer : ContentSniffer { manifest.contains("@context") && manifest.contains("https://www.w3.org/ns/pub-context") ) { - return Try.success(Format.LPF) + return Try.success( + if (manifest.contains("https://www.w3.org/TR/audiobooks/")) { + format.orEmpty() + Trait.LPF + Trait.AUDIOBOOK + } else { + format.orEmpty() + Trait.LPF + } + ) } } @@ -744,25 +721,12 @@ public object ArchiveSniffer : ContentSniffer { return Try.success(format) } - if ( - archiveContainsOnlyExtensions(cbzExtensions) && - format?.conformsTo(Format.ZIP) != false // Recognize exploded CBZ/CBR - ) { - return Try.success(Format.CBZ) - } - - if ( - archiveContainsOnlyExtensions(cbzExtensions) && - format?.conformsTo(Format.RAR) == true - ) { - return Try.success(Format.CBR) + if (archiveContainsOnlyExtensions(cbzExtensions)) { + return Try.success(format.orEmpty() + Trait.COMICS) } - if ( - archiveContainsOnlyExtensions(zabExtensions) && - format?.conformsTo(Format.ZIP) != false // Recognize exploded ZAB - ) { - return Try.success(Format.ZAB) + if (archiveContainsOnlyExtensions(zabExtensions)) { + return Try.success(format.orEmpty() + Trait.AUDIOBOOK) } return Try.success(format) @@ -820,7 +784,7 @@ public object JsonSniffer : ContentSniffer { } if (hints.hasMediaType("application/problem+json")) { - return Format.JSON_PROBLEM_DETAILS + return Format.JSON + Trait.JSON_PROBLEM_DETAILS } return format @@ -866,7 +830,7 @@ public object AdeptSniffer : ContentSniffer { ?.flatMap { it.get("KeyInfo", EpubEncryption.SIG) } ?.flatMap { it.get("resource", "http://ns.adobe.com/adept") } ?.takeIf { it.isNotEmpty() } - ?.let { return Try.success(Format.EPUB_ADEPT) } + ?.let { return Try.success(format + Trait.ADEPT_PROTECTED) } container[Url("META-INF/rights.xml")!!] ?.readDecodeOrElse( @@ -874,7 +838,7 @@ public object AdeptSniffer : ContentSniffer { recover = { null } ) ?.takeIf { it.namespace == "http://ns.adobe.com/adept" } - ?.let { return Try.success(Format.EPUB_ADEPT) } + ?.let { return Try.success(format + Trait.ADEPT_PROTECTED) } return Try.success(format) } @@ -889,22 +853,19 @@ public object LcpSniffer : ContentSniffer { format: Format?, container: Container ): Try { - when { - format?.conformsTo(Format.RPF) == true -> { + return when { + format?.conformsTo(Trait.RPF) == true -> { val isLcpProtected = RelativeUrl("license.lcpl")!! in container || hasLcpSchemeInManifest(container) .getOrElse { return Try.failure(it) } - if (isLcpProtected) { - val newFormat = when (format) { - Format.RPF_IMAGE -> Format.RPF_IMAGE_LCP - Format.RPF_AUDIO -> Format.RPF_AUDIO_LCP - Format.RPF_PDF -> Format.RPF_PDF_LCP - Format.RPF -> Format.RPF_LCP - else -> null + Try.success( + if (isLcpProtected) { + format + Trait.LCP_PROTECTED + } else { + format } - newFormat?.let { return Try.success(it) } - } + ) } format == Format.EPUB -> { @@ -912,13 +873,17 @@ public object LcpSniffer : ContentSniffer { hasLcpSchemeInEncryptionXml(container) .getOrElse { return Try.failure(it) } - if (isLcpProtected) { - return Try.success(Format.EPUB_LCP) - } + Try.success( + if (isLcpProtected) { + format + Trait.LCP_PROTECTED + } else { + format + } + ) } + else -> + Try.success(format) } - - return Try.success(format) } private suspend fun hasLcpSchemeInManifest(container: Container): Try { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt index a022982215..f0654cc7fc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt @@ -7,65 +7,157 @@ package org.readium.r2.shared.util.format @JvmInline -public value class Format(public val id: String) { +public value class Trait(private val id: String) { - public fun conformsTo(other: Format): Boolean { - val thisComponents = id.split(".") - val otherComponents = other.id.split(".") - return thisComponents.containsAll(otherComponents) + override fun toString(): String = id + + public companion object { + + public val ZIP: Trait = Trait("zip") + public val RAR: Trait = Trait("rar") + + public val JSON: Trait = Trait("json") + public val JSON_PROBLEM_DETAILS: Trait = Trait("json_problem_details") + public val LCP_LICENSE_DOCUMENT: Trait = Trait("lcp_license_document") + + public val W3C_PUB_MANIFEST: Trait = Trait("w3c_pub_manifest") + public val W3C_AUDIOBOOK_MANIFEST: Trait = Trait("w3c_audiobook_manifest") + + public val READIUM_WEBPUB_MANIFEST: Trait = Trait("readium_webpub_manifest") + public val READIUM_AUDIOBOOK_MANIFEST: Trait = Trait("readium_audiobook_manifest") + public val READIUM_COMICS_MANIFEST: Trait = Trait("readium_comics_manifest") + public val READIUM_PDF_MANIFEST: Trait = Trait("readium_pdf_manifest") + + public val XML: Trait = Trait("xml") + + public val LCP_PROTECTED: Trait = Trait("lcp") + public val ADEPT_PROTECTED: Trait = Trait("adept") + + public val PDF: Trait = Trait("pdf") + public val HTML: Trait = Trait("html") + public val AUDIO: Trait = Trait("audio") + public val BITMAP: Trait = Trait("bitmap") + + public val AVIF: Trait = Trait("avif") + public val BMP: Trait = Trait("bmp") + public val GIF: Trait = Trait("gif") + public val JPEG: Trait = Trait("jpeg") + public val JXL: Trait = Trait("jxl") + public val PNG: Trait = Trait("png") + public val TIFF: Trait = Trait("tiff") + public val WEBP: Trait = Trait("webp") + + public val EPUB: Trait = Trait("epub") + public val RPF: Trait = Trait("rpf") + public val LPF: Trait = Trait("lpf") + public val AUDIOBOOK: Trait = Trait("audiobook") + public val COMICS: Trait = Trait("comics") + public val PDFBOOK: Trait = Trait("pdfbook") + public val WEBPUB: Trait = Trait("webpub") + + public val OPDS1_CATALOG: Trait = Trait("opds1_catalog") + public val OPDS1_ENTRY: Trait = Trait("opds1_entry") + public val OPDS1_NAVIGATION_FEED: Trait = Trait("opds1_navigation_feed") + public val OPDS1_ACQUISITION_FEED: Trait = Trait("opds1_acquisition_feed") + + public val OPDS2_CATALOG: Trait = Trait("opds2_catalog") + public val OPDS2_PUBLICATION: Trait = Trait("opds2_publication") + public val OPDS_AUTHENTICATION: Trait = Trait("opds_authentication") + } +} + +@JvmInline +public value class Format(private val traits: Set) { + + public operator fun plus(trait: Trait): Format = + Format(traits + trait) + + public operator fun minus(trait: Trait): Format = + Format(traits - trait) + + public fun conformsTo(trait: Trait): Boolean = + trait in traits + + public fun conformsTo(format: Format): Boolean = + format.traits.all { it in this.traits } + + public override fun toString(): String { + return traits.joinToString(";") { it.toString() } } public companion object { - public val RAR: Format = Format("rar") - public val CBR: Format = Format("rar.image") - - public val ZIP: Format = Format("zip") - public val CBZ: Format = Format("zip.image") - public val ZAB: Format = Format("zip.audio") - public val LPF: Format = Format("zip.lpf") - public val EPUB: Format = Format("zip.epub") - public val EPUB_LCP: Format = Format("zip.epub.lcp") - public val EPUB_ADEPT: Format = Format("zip.epub.adept") - - public val RPF: Format = Format("zip.rpf") - public val RPF_AUDIO: Format = Format("zip.rpf.audio") - public val RPF_AUDIO_LCP: Format = Format("zip.rpf.audio.lcp") - public val RPF_IMAGE: Format = Format("zip.rpf.image") - public val RPF_IMAGE_LCP: Format = Format("zip.rpf.image.lcp") - public val RPF_PDF: Format = Format("zip.rpf.pdf") - public val RPF_PDF_LCP: Format = Format("zip.rpf.pdf.lcp") - public val RPF_LCP: Format = Format("zip.rpf.lcp") - - public val JSON: Format = Format("json") - public val JSON_PROBLEM_DETAILS: Format = Format("json.problem_details") - public val LCP_LICENSE_DOCUMENT: Format = Format("json.lcpl") - public val W3C_WPUB_MANIFEST: Format = Format("json.w3c_wp_manifest") - public val RWPM: Format = Format("json.rwpm") - public val RWPM_AUDIO: Format = Format("json.rwpm.audio") - public val RWPM_IMAGE: Format = Format("json.rwpm.image") - public val OPDS2: Format = Format("json.opds") - public val OPDS2_PUBLICATION: Format = Format("json.opds_publication") - public val OPDS_AUTHENTICATION: Format = Format("json.opds_authentication") - - public val PDF: Format = Format("pdf") - public val HTML: Format = Format("html") - - public val AVIF: Format = Format("avif") - public val BMP: Format = Format("bmp") - public val GIF: Format = Format("gif") - public val JPEG: Format = Format("jpeg") - public val JXL: Format = Format("jxl") - public val PNG: Format = Format("png") - public val TIFF: Format = Format("tiff") - public val WEBP: Format = Format("webp") - - public val XML: Format = Format("xml") - public val XHTML: Format = Format("xml.html") - public val ATOM: Format = Format("xml.atom") - public val OPDS1: Format = Format("xml.atom.opds") - public val OPDS1_ENTRY: Format = Format("xml.atom.opds_entry") - public val OPDS1_NAVIGATION_FEED: Format = Format("xml.atom.opds_navigation_feed") - public val OPDS1_ACQUISITION_FEED: Format = Format("xml.atom.opds_acquisition_feed") + public operator fun invoke(string: String): Format = + Format(string.split(";").map { Trait(it) }.toSet()) + + internal fun Format?.orEmpty() = this ?: Format(setOf()) + + public val ZIP: Format = Format(setOf(Trait.ZIP)) + public val RAR: Format = Format(setOf(Trait.RAR)) + public val LCP_LICENSE_DOCUMENT: Format = Format( + setOf(Trait.JSON, Trait.LCP_LICENSE_DOCUMENT) + ) + + public val READIUM_WEBPUB_MANIFEST: Format = Format( + setOf(Trait.JSON, Trait.READIUM_WEBPUB_MANIFEST) + ) + public val READIUM_AUDIOBOOK_MANIFEST: Format = Format( + setOf(Trait.JSON, Trait.READIUM_AUDIOBOOK_MANIFEST) + ) + public val READIUM_COMICS_MANIFEST: Format = Format( + setOf(Trait.JSON, Trait.READIUM_COMICS_MANIFEST) + ) + public val READIUM_PDF_MANIFEST: Format = Format( + setOf(Trait.JSON, Trait.READIUM_PDF_MANIFEST) + ) + + public val READIUM_WEBPUB: Format = Format(setOf(Trait.ZIP, Trait.RPF)) + public val READIUM_AUDIOBOOK: Format = Format(setOf(Trait.ZIP, Trait.RPF, Trait.AUDIOBOOK)) + public val READIUM_COMICS: Format = Format(setOf(Trait.ZIP, Trait.RPF, Trait.COMICS)) + public val READIUM_PDF: Format = Format(setOf(Trait.ZIP, Trait.RPF, Trait.PDFBOOK)) + + public val PDF: Format = Format(setOf(Trait.PDF)) + public val EPUB: Format = Format(setOf(Trait.ZIP, Trait.EPUB)) + public val CBZ: Format = Format(setOf(Trait.ZIP, Trait.COMICS)) + public val CBR: Format = Format(setOf(Trait.RAR, Trait.COMICS)) + public val ZAB: Format = Format(setOf(Trait.ZIP, Trait.AUDIOBOOK)) + + public val XML: Format = Format(setOf(Trait.XML)) + public val XHTML: Format = Format(setOf(Trait.XML, Trait.HTML)) + public val HTML: Format = Format(setOf(Trait.HTML)) + + public val AVIF: Format = Format(setOf(Trait.BITMAP, Trait.AVIF)) + public val BMP: Format = Format(setOf(Trait.BITMAP, Trait.BMP)) + public val GIF: Format = Format(setOf(Trait.BITMAP, Trait.GIF)) + public val JPEG: Format = Format(setOf(Trait.BITMAP, Trait.JPEG)) + public val JXL: Format = Format(setOf(Trait.BITMAP, Trait.JXL)) + public val PNG: Format = Format(setOf(Trait.BITMAP, Trait.PNG)) + public val TIFF: Format = Format(setOf(Trait.BITMAP, Trait.TIFF)) + public val WEBP: Format = Format(setOf(Trait.BITMAP, Trait.WEBP)) + + public val JSON: Format = Format(setOf(Trait.JSON)) + public val JSON_PROBLEM_DETAILS: Format = Format( + setOf(Trait.JSON, Trait.JSON_PROBLEM_DETAILS) + ) + + public val W3C_PUB_MANIFEST: Format = Format(setOf(Trait.JSON, Trait.W3C_PUB_MANIFEST)) + public val W3C_AUDIOBOOK_MANIFEST: Format = Format( + setOf(Trait.JSON, Trait.W3C_AUDIOBOOK_MANIFEST) + ) + + public val OPDS1_CATALOG: Format = Format(setOf(Trait.XML, Trait.OPDS1_CATALOG)) + public val OPDS1_ENTRY: Format = Format(setOf(Trait.XML, Trait.OPDS1_ENTRY)) + public val OPDS1_NAVIGATION_FEED: Format = Format( + setOf(Trait.XML, Trait.OPDS1_NAVIGATION_FEED) + ) + public val OPDS1_ACQUISITION_FEED: Format = Format( + setOf(Trait.XML, Trait.OPDS1_ACQUISITION_FEED) + ) + + public val OPDS2_CATALOG: Format = Format(setOf(Trait.JSON, Trait.OPDS2_CATALOG)) + public val OPDS2_PUBLICATION: Format = Format(setOf(Trait.JSON, Trait.OPDS2_PUBLICATION)) + public val OPDS_AUTHENTICATION: Format = Format( + setOf(Trait.JSON, Trait.OPDS_AUTHENTICATION) + ) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt index e4ace1f53a..b3969b5a6a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt @@ -24,21 +24,42 @@ public class FormatRegistry( formatInfo: Map = mapOf( Format.CBR to FormatInfo(MediaType.CBR, FileExtension("cbr")), Format.CBZ to FormatInfo(MediaType.CBZ, FileExtension("cbz")), - Format.RPF_IMAGE to FormatInfo(MediaType.DIVINA, FileExtension("divina")), - Format.RWPM_IMAGE to FormatInfo(MediaType.DIVINA_MANIFEST, FileExtension("json")), + Format(setOf(Trait.ZIP, Trait.RPF, Trait.COMICS)) to FormatInfo( + MediaType.DIVINA, + FileExtension("divina") + ), + Format.READIUM_COMICS_MANIFEST to FormatInfo( + MediaType.DIVINA_MANIFEST, + FileExtension("json") + ), Format.EPUB to FormatInfo(MediaType.EPUB, FileExtension("epub")), Format.LCP_LICENSE_DOCUMENT to FormatInfo( MediaType.LCP_LICENSE_DOCUMENT, FileExtension("lcpl") ), - Format.RPF_AUDIO_LCP to FormatInfo(MediaType.LCP_PROTECTED_AUDIOBOOK, FileExtension("lcpa")), - Format.RPF_PDF_LCP to FormatInfo(MediaType.LCP_PROTECTED_PDF, FileExtension("lcpdf")), - Format.PDF to FormatInfo(MediaType.PDF, FileExtension("pdf")), - Format.RPF_AUDIO to FormatInfo(MediaType.READIUM_AUDIOBOOK, FileExtension("audiobook")), - Format.RWPM_AUDIO to FormatInfo(MediaType.READIUM_AUDIOBOOK_MANIFEST, FileExtension("json")), - Format.RPF to FormatInfo(MediaType.READIUM_WEBPUB, FileExtension("webpub")), - Format.RWPM to FormatInfo(MediaType.READIUM_WEBPUB, FileExtension("json")), - Format.JPEG to FormatInfo(MediaType.JPEG, FileExtension("jpg")) + (Format.READIUM_AUDIOBOOK + Trait.LCP_PROTECTED) to FormatInfo( + MediaType.LCP_PROTECTED_AUDIOBOOK, + FileExtension("lcpa") + ), + (Format.READIUM_PDF + Trait.LCP_PROTECTED)to FormatInfo( + MediaType.LCP_PROTECTED_PDF, + FileExtension("lcpdf") + ), + Format(setOf(Trait.PDF)) to FormatInfo(MediaType.PDF, FileExtension("pdf")), + Format.READIUM_AUDIOBOOK to FormatInfo( + MediaType.READIUM_AUDIOBOOK, + FileExtension("audiobook") + ), + Format.READIUM_AUDIOBOOK_MANIFEST to FormatInfo( + MediaType.READIUM_AUDIOBOOK_MANIFEST, + FileExtension("json") + ), + Format.READIUM_WEBPUB to FormatInfo(MediaType.READIUM_WEBPUB, FileExtension("webpub")), + Format.READIUM_WEBPUB_MANIFEST to FormatInfo( + MediaType.READIUM_WEBPUB, + FileExtension("json") + ), + Format(setOf(Trait.BITMAP, Trait.JPEG)) to FormatInfo(MediaType.JPEG, FileExtension("jpg")) ) ) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index 4e799fc3f0..96bff8f64e 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -21,6 +21,7 @@ import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.Trait import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.Resource @@ -56,7 +57,7 @@ internal class FileZipArchiveProvider { format: Format, file: File ): Try, ArchiveOpener.OpenError> { - if (!format.conformsTo(Format.ZIP)) { + if (!format.conformsTo(Trait.ZIP)) { return Try.failure( ArchiveOpener.OpenError.FormatNotSupported(format) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index 3d848eed1b..a63ec3cc0c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -21,6 +21,7 @@ import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.Trait import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile @@ -47,7 +48,7 @@ internal class StreamingZipArchiveProvider { format: Format, source: Readable ): Try, ArchiveOpener.OpenError> { - if (!format.conformsTo(Format.ZIP)) { + if (!format.conformsTo(Trait.ZIP)) { return Try.failure( ArchiveOpener.OpenError.FormatNotSupported(format) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt index 9b0003c93a..ca5f53272d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt @@ -29,7 +29,9 @@ public class ZipArchiveOpener : ArchiveOpener { ?.let { fileZipArchiveProvider.open(format, it) } ?: streamingZipArchiveProvider.open(format, source) - override suspend fun sniffOpen(source: Readable): Try { + override suspend fun sniffOpen( + source: Readable + ): Try { (source as? Resource)?.sourceUrl?.toFile() ?.let { return fileZipArchiveProvider.sniff(it) } diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/format/DefaultSniffersTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/format/DefaultSniffersTest.kt index 339da420c4..0e3f42a11b 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/format/DefaultSniffersTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/format/DefaultSniffersTest.kt @@ -33,7 +33,7 @@ class DefaultSniffersTest { @Test fun `Sniff Adobe ADEPT`() = runBlocking { assertEquals( - Format.EPUB_ADEPT, + Format.EPUB + Trait.ADEPT_PROTECTED, AdeptSniffer.sniffContainer( format = Format.EPUB, container = TestContainer( @@ -56,7 +56,7 @@ class DefaultSniffersTest { @Test fun `Sniff Adobe ADEPT from rights xml`() = runBlocking { assertEquals( - Format.EPUB_ADEPT, + Format.EPUB + Trait.ADEPT_PROTECTED, AdeptSniffer.sniffContainer( format = Format.EPUB, container = TestContainer( @@ -84,9 +84,9 @@ class DefaultSniffersTest { @Test fun `Sniff LCP protected Readium package`() = runBlocking { assertEquals( - Format.RPF_LCP, + Format.READIUM_WEBPUB + Trait.LCP_PROTECTED, LcpSniffer.sniffContainer( - format = Format.RPF, + format = Format.READIUM_WEBPUB, container = TestContainer(Url("license.lcpl")!! to "{}") ).checkSuccess() ) @@ -95,7 +95,7 @@ class DefaultSniffersTest { @Test fun `Sniff LCP protected EPUB`() = runBlocking { assertEquals( - Format.EPUB_LCP, + Format.EPUB + Trait.LCP_PROTECTED, LcpSniffer.sniffContainer( format = Format.EPUB, container = TestContainer(Url("META-INF/license.lcpl")!! to "{}") @@ -106,7 +106,7 @@ class DefaultSniffersTest { @Test fun `Sniff LCP protected EPUB missing the license`() = runBlocking { assertEquals( - Format.EPUB_LCP, + Format.EPUB + Trait.LCP_PROTECTED, LcpSniffer.sniffContainer( format = Format.EPUB, container = TestContainer( diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt index 299a827c16..1d58b18788 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt @@ -19,6 +19,7 @@ import org.readium.r2.shared.util.data.EmptyContainer import org.readium.r2.shared.util.format.FileExtension import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatHints +import org.readium.r2.shared.util.format.Trait import org.readium.r2.shared.util.resource.StringResource import org.robolectric.RobolectricTestRunner @@ -72,7 +73,7 @@ class AssetSnifferTest { SniffError.NotRecognized ) assertEquals( - Format.RPF_AUDIO, + Format.READIUM_AUDIOBOOK, sniffer.sniffFileExtension("audiobook").checkSuccess() ) assertEquals( @@ -80,11 +81,11 @@ class AssetSnifferTest { SniffError.NotRecognized ) assertEquals( - Format.RPF_AUDIO, + Format.READIUM_AUDIOBOOK, sniffer.sniffMediaType("application/audiobook+zip").checkSuccess() ) assertEquals( - Format.RPF_AUDIO, + Format.READIUM_AUDIOBOOK, sniffer.sniffHints( FormatHints( mediaTypes = listOf("application/audiobook+zip"), @@ -97,7 +98,7 @@ class AssetSnifferTest { @Test fun `sniff from bytes`() = runBlocking { assertEquals( - Format.RWPM_AUDIO, + Format.READIUM_AUDIOBOOK_MANIFEST, sniffer.sniff(fixtures.fileAt("audiobook.json")).checkSuccess() ) } @@ -117,15 +118,15 @@ class AssetSnifferTest { @Test fun `sniff audiobook`() = runBlocking { assertEquals( - Format.RPF_AUDIO, + Format.READIUM_AUDIOBOOK, sniffer.sniffFileExtension("audiobook").checkSuccess() ) assertEquals( - Format.RPF_AUDIO, + Format.READIUM_AUDIOBOOK, sniffer.sniffMediaType("application/audiobook+zip").checkSuccess() ) assertEquals( - Format.RPF_AUDIO, + Format.READIUM_AUDIOBOOK, sniffer.sniff(fixtures.fileAt("audiobook-package.unknown")).checkSuccess() ) } @@ -133,15 +134,15 @@ class AssetSnifferTest { @Test fun `sniff audiobook manifest`() = runBlocking { assertEquals( - Format.RWPM_AUDIO, + Format.READIUM_AUDIOBOOK_MANIFEST, sniffer.sniffMediaType("application/audiobook+json").checkSuccess() ) assertEquals( - Format.RWPM_AUDIO, + Format.READIUM_AUDIOBOOK_MANIFEST, sniffer.sniff(fixtures.fileAt("audiobook.json")).checkSuccess() ) assertEquals( - Format.RWPM_AUDIO, + Format.READIUM_AUDIOBOOK_MANIFEST, sniffer.sniff(fixtures.fileAt("audiobook-wrongtype.json")).checkSuccess() ) } @@ -181,15 +182,15 @@ class AssetSnifferTest { @Test fun `sniff DiViNa`() = runBlocking { assertEquals( - Format.RPF_IMAGE, + Format.READIUM_COMICS, sniffer.sniffFileExtension("divina").checkSuccess() ) assertEquals( - Format.RPF_IMAGE, + Format.READIUM_COMICS, sniffer.sniffMediaType("application/divina+zip").checkSuccess() ) assertEquals( - Format.RPF_IMAGE, + Format.READIUM_COMICS, sniffer.sniff(fixtures.fileAt("divina-package.unknown")).checkSuccess() ) } @@ -197,11 +198,11 @@ class AssetSnifferTest { @Test fun `sniff DiViNa manifest`() = runBlocking { assertEquals( - Format.RWPM_IMAGE, + Format.READIUM_COMICS_MANIFEST, sniffer.sniffMediaType("application/divina+json").checkSuccess() ) assertEquals( - Format.RWPM_IMAGE, + Format.READIUM_COMICS_MANIFEST, sniffer.sniff(fixtures.fileAt("divina.json")).checkSuccess() ) } @@ -318,7 +319,7 @@ class AssetSnifferTest { @Test fun `sniff OPDS 1 feed`() = runBlocking { assertEquals( - Format.OPDS1, + Format.OPDS1_CATALOG, sniffer.sniffMediaType("application/atom+xml;profile=opds-catalog").checkSuccess() ) assertEquals( @@ -330,7 +331,7 @@ class AssetSnifferTest { sniffer.sniffMediaType("application/atom+xml;profile=opds-catalog;kind=acquisition").checkSuccess() ) assertEquals( - Format.OPDS1, + Format.OPDS1_CATALOG, sniffer.sniff(fixtures.fileAt("opds1-feed.unknown")).checkSuccess() ) } @@ -350,11 +351,11 @@ class AssetSnifferTest { @Test fun `sniff OPDS 2 feed`() = runBlocking { assertEquals( - Format.OPDS2, + Format.OPDS2_CATALOG, sniffer.sniffMediaType("application/opds+json").checkSuccess() ) assertEquals( - Format.OPDS2, + Format.OPDS2_CATALOG, sniffer.sniff(fixtures.fileAt("opds2-feed.json")).checkSuccess() ) } @@ -390,15 +391,15 @@ class AssetSnifferTest { @Test fun `sniff LCP protected audiobook`() = runBlocking { assertEquals( - Format.RPF_AUDIO_LCP, + Format.READIUM_AUDIOBOOK + Trait.LCP_PROTECTED, sniffer.sniffFileExtension("lcpa").checkSuccess() ) assertEquals( - Format.RPF_AUDIO_LCP, + Format.READIUM_AUDIOBOOK + Trait.LCP_PROTECTED, sniffer.sniffMediaType("application/audiobook+lcp").checkSuccess() ) assertEquals( - Format.RPF_AUDIO_LCP, + Format.READIUM_AUDIOBOOK + Trait.LCP_PROTECTED, sniffer.sniff(fixtures.fileAt("audiobook-lcp.unknown")).checkSuccess() ) } @@ -406,15 +407,15 @@ class AssetSnifferTest { @Test fun `sniff LCP protected PDF`() = runBlocking { assertEquals( - Format.RPF_PDF_LCP, + Format.READIUM_PDF + Trait.LCP_PROTECTED, sniffer.sniffFileExtension("lcpdf").checkSuccess() ) assertEquals( - Format.RPF_PDF_LCP, + Format.READIUM_PDF + Trait.LCP_PROTECTED, sniffer.sniffMediaType("application/pdf+lcp").checkSuccess() ) assertEquals( - Format.RPF_PDF_LCP, + Format.READIUM_PDF + Trait.LCP_PROTECTED, sniffer.sniff(fixtures.fileAt("pdf-lcp.unknown")).checkSuccess() ) } @@ -438,19 +439,19 @@ class AssetSnifferTest { @Test fun `sniff LPF`() = runBlocking { assertEquals( - Format.LPF, + Format(setOf(Trait.ZIP, Trait.LPF)), sniffer.sniffFileExtension("lpf").checkSuccess() ) assertEquals( - Format.LPF, + Format(setOf(Trait.ZIP, Trait.LPF)), sniffer.sniffMediaType("application/lpf+zip").checkSuccess() ) assertEquals( - Format.LPF, + Format(setOf(Trait.ZIP, Trait.LPF)), sniffer.sniff(fixtures.fileAt("lpf.unknown")).checkSuccess() ) assertEquals( - Format.LPF, + Format(setOf(Trait.ZIP, Trait.LPF)), sniffer.sniff(fixtures.fileAt("lpf-index-html.unknown")).checkSuccess() ) } @@ -494,15 +495,15 @@ class AssetSnifferTest { @Test fun `sniff WebPub`() = runBlocking { assertEquals( - Format.RPF, + Format.READIUM_WEBPUB, sniffer.sniffFileExtension("webpub").checkSuccess() ) assertEquals( - Format.RPF, + Format.READIUM_WEBPUB, sniffer.sniffMediaType("application/webpub+zip").checkSuccess() ) assertEquals( - Format.RPF, + Format.READIUM_WEBPUB, sniffer.sniff(fixtures.fileAt("webpub-package.unknown")).checkSuccess() ) } @@ -510,11 +511,11 @@ class AssetSnifferTest { @Test fun `sniff WebPub manifest`() = runBlocking { assertEquals( - Format.RWPM, + Format.READIUM_WEBPUB_MANIFEST, sniffer.sniffMediaType("application/webpub+json").checkSuccess() ) assertEquals( - Format.RWPM, + Format.READIUM_WEBPUB_MANIFEST, sniffer.sniff(fixtures.fileAt("webpub.json")).checkSuccess() ) } @@ -522,7 +523,7 @@ class AssetSnifferTest { @Test fun `sniff W3C WPUB manifest`() = runBlocking { assertEquals( - Format.W3C_WPUB_MANIFEST, + Format(setOf(Trait.JSON, Trait.W3C_PUB_MANIFEST)), sniffer.sniff(fixtures.fileAt("w3c-wpub.json")).checkSuccess() ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index 30472522c0..0182a8f55a 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -26,6 +26,7 @@ import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.decodeRwpm import org.readium.r2.shared.util.data.readDecodeOrElse import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.Trait import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.http.HttpContainer @@ -50,13 +51,21 @@ public class ReadiumWebPubParser( asset: Asset, warnings: WarningLogger? ): Try { - if (asset is ResourceAsset && asset.format.conformsTo(Format.RWPM)) { + if ( + asset is ResourceAsset && + ( + asset.format.conformsTo(Format.READIUM_WEBPUB_MANIFEST) || + asset.format.conformsTo(Trait.READIUM_PDF_MANIFEST) || + asset.format.conformsTo(Trait.READIUM_AUDIOBOOK_MANIFEST) || + asset.format.conformsTo(Trait.READIUM_COMICS_MANIFEST) + ) + ) { val packageAsset = createPackage(asset) .getOrElse { return Try.failure(it) } return parse(packageAsset, warnings) } - if (asset !is ContainerAsset || !asset.format.conformsTo(Format.RPF)) { + if (asset !is ContainerAsset || !asset.format.conformsTo(Trait.RPF)) { return Try.failure(PublicationParser.Error.FormatNotSupported()) } @@ -78,7 +87,7 @@ public class ReadiumWebPubParser( // Checks the requirements from the LCPDF specification. // https://readium.org/lcp-specs/notes/lcp-for-pdf.html val readingOrder = manifest.readingOrder - if (asset.format.conformsTo(Format.RPF_PDF_LCP) && + if (asset.format.conformsTo(Trait.PDFBOOK) && asset.format.conformsTo(Trait.LCP_PROTECTED) && (readingOrder.isEmpty() || !readingOrder.all { MediaType.PDF.matches(it.mediaType) }) ) { return Try.failure( @@ -91,17 +100,19 @@ public class ReadiumWebPubParser( val servicesBuilder = Publication.ServicesBuilder().apply { cacheServiceFactory = InMemoryCacheService.createFactory(context) - positionsServiceFactory = when (asset.format) { - Format.RPF_PDF_LCP -> + positionsServiceFactory = when { + asset.format.conformsTo(Trait.PDFBOOK) && asset.format.conformsTo( + Trait.LCP_PROTECTED + ) -> pdfFactory?.let { LcpdfPositionsService.create(it) } - Format.RPF_IMAGE -> + asset.format.conformsTo(Trait.COMICS) -> PerResourcePositionsService.createFactory(MediaType("image/*")!!) else -> WebPositionsService.createFactory(httpClient) } locatorServiceFactory = when { - asset.format.conformsTo(Format.RPF_AUDIO) -> + asset.format.conformsTo(Trait.AUDIOBOOK) -> AudioLocatorService.createFactory() else -> null @@ -154,7 +165,18 @@ public class ReadiumWebPubParser( return Try.success( ContainerAsset( - format = Format.RPF, + format = when { + asset.format.conformsTo(Trait.READIUM_AUDIOBOOK_MANIFEST) -> + Format.READIUM_AUDIOBOOK + asset.format.conformsTo(Trait.READIUM_COMICS_MANIFEST) -> + Format.READIUM_COMICS + asset.format.conformsTo(Trait.READIUM_WEBPUB_MANIFEST) -> + Format.READIUM_WEBPUB + asset.format.conformsTo(Trait.READIUM_PDF_MANIFEST) -> + Format.READIUM_PDF + else -> + throw IllegalStateException() + }, container = container ) ) diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt b/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt index 2ba585eb55..13ee1b0016 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt @@ -57,7 +57,7 @@ data class Book( author = author, identifier = identifier, progression = progression, - formatId = format.id, + formatId = format.toString(), rawMediaType = mediaType.toString(), cover = cover ) From 921fe1083219788f6b5ec20571e84f5f93c9b980 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 18 Dec 2023 12:08:25 +0100 Subject: [PATCH 79/86] Fill encryption data in LcpContentProtection --- .../readium/r2/lcp/EpubEncryptionParser.kt | 75 +++++++++++++++++++ .../readium/r2/lcp/LcpContentProtection.kt | 57 +++++++++++--- .../java/org/readium/r2/lcp/LcpDecryptor.kt | 2 +- .../streamer/parser/epub/EpubDeobfuscator.kt | 6 +- .../r2/streamer/parser/epub/EpubParser.kt | 11 +-- .../parser/epub/EpubDeobfuscatorTest.kt | 13 ++-- 6 files changed, 137 insertions(+), 27 deletions(-) create mode 100644 readium/lcp/src/main/java/org/readium/r2/lcp/EpubEncryptionParser.kt diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/EpubEncryptionParser.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/EpubEncryptionParser.kt new file mode 100644 index 0000000000..5446f8a0e2 --- /dev/null +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/EpubEncryptionParser.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2022 Readium Foundation. All rights reserved. + * Use of this source code is governed by the BSD-style license + * available in the top-level LICENSE file of the project. + */ + +package org.readium.r2.lcp + +import org.readium.r2.shared.publication.encryption.Encryption +import org.readium.r2.shared.publication.protection.ContentProtection +import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.xml.ElementNode + +internal object EpubEncryptionParser { + + object Namespaces { + const val ENC = "http://www.w3.org/2001/04/xmlenc#" + const val SIG = "http://www.w3.org/2000/09/xmldsig#" + const val COMP = "http://www.idpf.org/2016/encryption#compression" + } + + fun parse(document: ElementNode): Map = + document.get("EncryptedData", Namespaces.ENC) + .mapNotNull { parseEncryptedData(it) } + .toMap() + + private fun parseEncryptedData(node: ElementNode): Pair? { + val resourceURI = node.getFirst("CipherData", Namespaces.ENC) + ?.getFirst("CipherReference", Namespaces.ENC)?.getAttr("URI") + ?.let { Url(it) ?: Url.fromDecodedPath(it) } + ?: return null + val retrievalMethod = node.getFirst("KeyInfo", Namespaces.SIG) + ?.getFirst("RetrievalMethod", Namespaces.SIG)?.getAttr("URI") + val scheme = if (retrievalMethod == "license.lcpl#/encryption/content_key") { + ContentProtection.Scheme.Lcp.uri + } else { + null + } + val algorithm = node.getFirst("EncryptionMethod", Namespaces.ENC) + ?.getAttr("Algorithm") + ?: return null + val compression = node.getFirst("EncryptionProperties", Namespaces.ENC) + ?.let { parseEncryptionProperties(it) } + val originalLength = compression?.first + val compressionMethod = compression?.second + val enc = Encryption( + scheme = scheme, + /* profile = drm?.license?.encryptionProfile, + FIXME: This has probably never worked. Profile needs to be filled somewhere, though. */ + algorithm = algorithm, + compression = compressionMethod, + originalLength = originalLength + ) + return Pair(resourceURI, enc) + } + + private fun parseEncryptionProperties(encryptionProperties: ElementNode): Pair? { + for (encryptionProperty in encryptionProperties.get("EncryptionProperty", Namespaces.ENC)) { + val compressionElement = encryptionProperty.getFirst("Compression", Namespaces.COMP) + if (compressionElement != null) { + parseCompressionElement(compressionElement)?.let { return it } + } + } + return null + } + + private fun parseCompressionElement(compressionElement: ElementNode): Pair? { + val originalLength = compressionElement.getAttr("OriginalLength")?.toLongOrNull() + ?: return null + val method = compressionElement.getAttr("Method") + ?: return null + val compression = if (method == "8") "deflate" else "none" + return Pair(originalLength, compression) + } +} diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index 9a5f948968..1154203195 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -8,23 +8,29 @@ 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.publication.encryption.Encryption 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.util.AbsoluteUrl import org.readium.r2.shared.util.DebugError 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.asset.Asset import org.readium.r2.shared.util.asset.AssetOpener import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.ResourceAsset +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError +import org.readium.r2.shared.util.data.decodeRwpm +import org.readium.r2.shared.util.data.decodeXml +import org.readium.r2.shared.util.data.readDecodeOrElse import org.readium.r2.shared.util.flatMap import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.Trait import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.TransformingContainer internal class LcpContentProtection( @@ -75,14 +81,21 @@ internal class LcpContentProtection( return lcpService.retrieveLicense(asset, authentication, allowUserInteraction) } - private fun createResultAsset( + private suspend fun createResultAsset( asset: ContainerAsset, license: Try ): Try { val serviceFactory = LcpContentProtectionService .createFactory(license.getOrNull(), license.failureOrNull()) - val decryptor = LcpDecryptor(license.getOrNull()) + val encryptionData = + when { + asset.format.conformsTo(Trait.EPUB) -> parseEncryptionDataEpub(asset.container) + else -> parseEncryptionDataRpf(asset.container) + } + .getOrElse { return Try.failure(ContentProtection.OpenError.Reading(it)) } + + val decryptor = LcpDecryptor(license.getOrNull(), encryptionData) val container = TransformingContainer(asset.container, decryptor::transform) @@ -92,13 +105,6 @@ internal class LcpContentProtection( container = container ), onCreatePublication = { - decryptor.encryptionData = (manifest.readingOrder + manifest.resources + manifest.links) - .flatten() - .mapNotNull { - it.properties.encryption?.let { enc -> it.url() to enc } - } - .toMap() - servicesBuilder.contentProtectionServiceFactory = serviceFactory } ) @@ -106,6 +112,37 @@ internal class LcpContentProtection( return Try.success(protectedFile) } + private suspend fun parseEncryptionDataEpub(container: Container): Try, ReadError> { + val encryptionResource = container[Url("META-INF/encryption.xml")!!] + ?: return Try.failure(ReadError.Decoding("Missing encryption.xml")) + + val encryptionDocument = encryptionResource + .readDecodeOrElse( + decode = { it.decodeXml() }, + recover = { return Try.failure(it) } + ) + + return Try.success(EpubEncryptionParser.parse(encryptionDocument)) + } + + private suspend fun parseEncryptionDataRpf(container: Container): Try, ReadError> { + val manifestResource = container[Url("manifest.json")!!] + ?: return Try.failure(ReadError.Decoding("Missing manifest")) + + val manifest = manifestResource + .readDecodeOrElse( + decode = { it.decodeRwpm() }, + recover = { return Try.failure(it) } + ) + + val encryptionData = manifest + .let { (it.readingOrder + it.resources) } + .mapNotNull { link -> link.properties.encryption?.let { link.url() to it } } + .toMap() + + return Try.success(encryptionData) + } + private suspend fun openLicense( licenseAsset: ResourceAsset, credentials: String?, diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt index 759f1e5f0c..5f15df16fc 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt @@ -29,7 +29,7 @@ import org.readium.r2.shared.util.resource.flatMap */ internal class LcpDecryptor( val license: LcpLicense?, - var encryptionData: Map = emptyMap() + val encryptionData: Map ) { fun transform(url: Url, resource: Resource): Resource { diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt index 15c84da219..c0dc70b29b 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscator.kt @@ -21,13 +21,15 @@ import org.readium.r2.shared.util.resource.flatMap */ internal class EpubDeobfuscator( private val pubId: String, - private val retrieveEncryption: (Url) -> Encryption? + private val encryptionData: Map ) { @Suppress("Unused_parameter") fun transform(url: Url, resource: Resource): Resource = resource.flatMap { - val algorithm = resource.sourceUrl?.let(retrieveEncryption)?.algorithm + val algorithm = resource.sourceUrl + ?.let { encryptionData[it] } + ?.algorithm if (algorithm != null && algorithm2length.containsKey(algorithm)) { DeobfuscatingResource(resource, algorithm) } else { diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index d214879607..88581fba1a 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -10,7 +10,6 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.encryption.Encryption -import org.readium.r2.shared.publication.encryption.encryption import org.readium.r2.shared.publication.services.content.DefaultContentService import org.readium.r2.shared.publication.services.content.iterators.HtmlResourceContentIterator import org.readium.r2.shared.publication.services.search.StringSearchService @@ -81,20 +80,18 @@ public class EpubParser( ) ) + val encryptionData = parseEncryptionData(asset.container) + val manifest = ManifestAdapter( packageDocument = packageDocument, navigationData = parseNavigationData(packageDocument, asset.container), - encryptionData = parseEncryptionData(asset.container), + encryptionData = encryptionData, displayOptions = parseDisplayOptions(asset.container) ).adapt() var container = asset.container manifest.metadata.identifier?.let { id -> - val deobfuscator = EpubDeobfuscator(id) { url -> - manifest.linkWithHref(url) - ?.properties?.encryption - } - + val deobfuscator = EpubDeobfuscator(id, encryptionData) container = TransformingContainer(container, deobfuscator::transform) } diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt index 9cd82f2b99..4e4c2c8227 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EpubDeobfuscatorTest.kt @@ -44,15 +44,14 @@ class EpubDeobfuscatorTest { .checkSuccess() private fun deobfuscate(url: Url, resource: Resource, algorithm: String?): Resource { - val deobfuscator = EpubDeobfuscator(identifier) { - if (resource.sourceUrl == it) { - algorithm?.let { - Encryption(algorithm = algorithm) - } + val encryptionData = + if (resource.sourceUrl != null && algorithm != null) { + mapOf(resource.sourceUrl as Url to Encryption(algorithm = algorithm)) } else { - null + emptyMap() } - } + + val deobfuscator = EpubDeobfuscator(identifier, encryptionData) return deobfuscator.transform(url, resource) } From cec64053f3846c666dc6c11cd0fab29884aa86b1 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Mon, 18 Dec 2023 19:48:08 +0100 Subject: [PATCH 80/86] Remove scheme in ContentProtection --- .../readium/r2/lcp/LcpContentProtection.kt | 3 -- .../AdeptFallbackContentProtection.kt | 45 ------------------- .../protection/ContentProtection.kt | 2 - ...ection.kt => FallbackContentProtection.kt} | 23 +++++----- .../readium/r2/streamer/PublicationFactory.kt | 27 ++++++----- 5 files changed, 24 insertions(+), 76 deletions(-) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt rename readium/shared/src/main/java/org/readium/r2/shared/publication/protection/{LcpFallbackContentProtection.kt => FallbackContentProtection.kt} (72%) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index 1154203195..de9e8cf389 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -39,9 +39,6 @@ internal class LcpContentProtection( private val assetOpener: AssetOpener ) : ContentProtection { - override val scheme: ContentProtection.Scheme = - ContentProtection.Scheme.Lcp - override suspend fun open( asset: Asset, credentials: String?, diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt deleted file mode 100644 index 96282568cb..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/AdeptFallbackContentProtection.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.publication.protection - -import org.readium.r2.shared.InternalReadiumApi -import org.readium.r2.shared.publication.protection.ContentProtection.Scheme -import org.readium.r2.shared.publication.services.contentProtectionServiceFactory -import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.asset.ContainerAsset -import org.readium.r2.shared.util.format.Trait - -/** - * [ContentProtection] implementation used as a fallback by the Streamer to detect Adept DRM, - * if it is not supported by the app. - */ -@InternalReadiumApi -public class AdeptFallbackContentProtection : ContentProtection { - - override val scheme: Scheme = Scheme.Adept - - override suspend fun open( - asset: Asset, - credentials: String?, - allowUserInteraction: Boolean - ): Try { - if (asset !is ContainerAsset || !asset.format.conformsTo(Trait.ADEPT_PROTECTED)) { - return Try.failure(ContentProtection.OpenError.AssetNotSupported()) - } - - val protectedFile = ContentProtection.OpenResult( - asset = asset, - onCreatePublication = { - servicesBuilder.contentProtectionServiceFactory = - FallbackContentProtectionService.createFactory(scheme, "Adobe ADEPT") - } - ) - - return Try.success(protectedFile) - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index 6c6575ddb1..e536aa7c81 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -54,8 +54,6 @@ public interface ContentProtection { val onCreatePublication: Publication.Builder.() -> Unit = {} ) - public val scheme: Scheme - /** * Attempts to unlock a potentially protected publication asset. * diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt similarity index 72% rename from readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt rename to readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt index 056674dcb3..eb9d0c416c 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/LcpFallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt @@ -19,33 +19,32 @@ import org.readium.r2.shared.util.format.Trait * if it is not supported by the app. */ @InternalReadiumApi -public class LcpFallbackContentProtection : ContentProtection { - - override val scheme: Scheme = - Scheme.Lcp +public class FallbackContentProtection : ContentProtection { override suspend fun open( asset: Asset, credentials: String?, allowUserInteraction: Boolean ): Try { - if ( - !asset.format.conformsTo(Trait.LCP_PROTECTED) - ) { - return Try.failure(ContentProtection.OpenError.AssetNotSupported()) - } - if (asset !is ContainerAsset) { return Try.failure( ContentProtection.OpenError.AssetNotSupported() ) } + val protectionServiceFactory = when { + asset.format.conformsTo(Trait.LCP_PROTECTED) -> + FallbackContentProtectionService.createFactory(Scheme.Lcp, "Readium LCP") + asset.format.conformsTo(Trait.ADEPT_PROTECTED) -> + FallbackContentProtectionService.createFactory(Scheme.Adept, "Adobe ADEPT") + else -> + return Try.failure(ContentProtection.OpenError.AssetNotSupported()) + } + val protectedFile = ContentProtection.OpenResult( asset = asset, onCreatePublication = { - servicesBuilder.contentProtectionServiceFactory = - FallbackContentProtectionService.createFactory(scheme, "Readium LCP") + servicesBuilder.contentProtectionServiceFactory = protectionServiceFactory } ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index 4e3cd4bbcd..ab63caafa8 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -9,9 +9,8 @@ package org.readium.r2.streamer import android.content.Context import org.readium.r2.shared.PdfSupport import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.protection.AdeptFallbackContentProtection import org.readium.r2.shared.publication.protection.ContentProtection -import org.readium.r2.shared.publication.protection.LcpFallbackContentProtection +import org.readium.r2.shared.publication.protection.FallbackContentProtection import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try @@ -74,11 +73,7 @@ public class PublicationFactory( } private val contentProtections: List = - buildList { - add(LcpFallbackContentProtection()) - add(AdeptFallbackContentProtection()) - addAll(contentProtections.asReversed()) - } + contentProtections + FallbackContentProtection() private val defaultParsers: List = listOfNotNull( @@ -130,7 +125,8 @@ public class PublicationFactory( var transformedAsset: Asset = asset for (protection in contentProtections) { - protection.open(asset, credentials, allowUserInteraction) + val openResult = protection + .open(asset, credentials, allowUserInteraction) .getOrElse { when (it) { is ContentProtection.OpenError.Reading -> @@ -138,13 +134,16 @@ public class PublicationFactory( is ContentProtection.OpenError.AssetNotSupported -> null } - }?.let { openResult -> - transformedAsset = openResult.asset - compositeOnCreatePublication = { - openResult.onCreatePublication.invoke(this) - compositeOnCreatePublication(this) - } } + + if (openResult != null) { + transformedAsset = openResult.asset + compositeOnCreatePublication = { + openResult.onCreatePublication.invoke(this) + compositeOnCreatePublication(this) + } + break + } } val builder = parse(transformedAsset, warnings) From b07488f003ea38be9edc002e129f7c6869f1ddba Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Tue, 19 Dec 2023 07:08:21 +0100 Subject: [PATCH 81/86] Various changes --- .../readium/r2/lcp/LcpPublicationRetriever.kt | 18 +-- .../java/org/readium/r2/lcp/LcpService.kt | 9 +- .../readium/r2/lcp/service/LicensesService.kt | 19 +-- .../publication/protection/EpubEncryption.kt | 14 --- .../protection/FallbackContentProtection.kt | 4 +- .../r2/shared/util/asset/AssetOpener.kt | 5 +- .../r2/shared/util/asset/AssetSniffer.kt | 118 +++++++++++------- .../r2/shared/util/format/DefaultSniffers.kt | 56 ++++++--- .../{ContentSniffer.kt => FormatSniffer.kt} | 2 +- .../r2/shared/util/zip/FileZipContainer.kt | 2 + .../shared/util/mediatype/AssetSnifferTest.kt | 2 +- .../readium/r2/streamer/PublicationFactory.kt | 12 +- .../r2/streamer/extensions/Container.kt | 34 +++++ .../r2/streamer/parser/PublicationParser.kt | 8 +- .../r2/streamer/parser/audio/AudioParser.kt | 54 +++----- .../r2/streamer/parser/epub/EpubParser.kt | 18 +-- .../r2/streamer/parser/image/ImageParser.kt | 54 +++----- .../r2/streamer/parser/pdf/PdfParser.kt | 6 +- .../parser/readium/ReadiumWebPubParser.kt | 18 +-- 19 files changed, 244 insertions(+), 209 deletions(-) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/publication/protection/EpubEncryption.kt rename readium/shared/src/main/java/org/readium/r2/shared/util/format/{ContentSniffer.kt => FormatSniffer.kt} (97%) diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt index 0219564c3e..43eb85045a 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpPublicationRetriever.kt @@ -20,6 +20,7 @@ import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatHints import org.readium.r2.shared.util.format.FormatRegistry +import org.readium.r2.shared.util.format.Trait import org.readium.r2.shared.util.getOrElse /** @@ -194,7 +195,7 @@ public class LcpPublicationRetriever( } downloadsRepository.removeDownload(requestId.value) - val formatWithoutLicense = + val baseFormat = assetSniffer.sniff( download.file, FormatHints( @@ -205,9 +206,11 @@ public class LcpPublicationRetriever( ) ).getOrElse { Format.EPUB } + val format = baseFormat + Trait.LCP_PROTECTED + try { // Saves the License Document into the downloaded publication - val container = createLicenseContainer(download.file, formatWithoutLicense) + val container = createLicenseContainer(download.file, format) container.write(license) } catch (e: Exception) { tryOrLog { download.file.delete() } @@ -217,17 +220,6 @@ public class LcpPublicationRetriever( return@launch } - val format = assetSniffer.sniff( - download.file, - FormatHints( - format = formatWithoutLicense, - mediaTypes = listOfNotNull( - license.publicationLink.mediaType, - download.mediaType - ) - ) - ).getOrElse { formatWithoutLicense } - val acquiredPublication = LcpService.AcquiredPublication( localFile = download.file, suggestedFilename = "${license.id}.${format.fileExtension}", diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt index 4047af316d..b6a2413e7a 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt @@ -42,13 +42,12 @@ public interface LcpService { /** * Returns if the file is a LCP license document or a publication protected by LCP. */ + @Deprecated( + "Use an AssetSniffer and check the returned format for Trait.LCP_PROTECTED", + level = DeprecationLevel.ERROR + ) public suspend fun isLcpProtected(file: File): Boolean - /** - * Returns if the asset is a LCP license document or a publication protected by LCP. - */ - public suspend fun isLcpProtected(asset: Asset): Boolean - /** * Acquires a protected publication from a standalone LCPL's bytes. * diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index 8a3d9a1555..f9a79a4d08 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -44,6 +44,7 @@ import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatHints import org.readium.r2.shared.util.format.FormatRegistry +import org.readium.r2.shared.util.format.Trait import org.readium.r2.shared.util.getOrElse import timber.log.Timber @@ -61,23 +62,23 @@ internal class LicensesService( private val formatRegistry = FormatRegistry() + @Deprecated( + "Use an AssetSniffer and check the returned format for Trait.LCP_PROTECTED", + level = DeprecationLevel.ERROR + ) override suspend fun isLcpProtected(file: File): Boolean { val asset = assetOpener.open(file) .getOrElse { return false } - return isLcpProtected(asset) - } - override suspend fun isLcpProtected(asset: Asset): Boolean = - tryOr(false) { + return tryOr(false) { when (asset) { is ResourceAsset -> - asset.format.conformsTo(Format.LCP_LICENSE_DOCUMENT) - is ContainerAsset -> { - createLicenseContainer(context, asset.container, asset.format).read() - true - } + asset.format.conformsTo(Trait.LCP_LICENSE_DOCUMENT) + is ContainerAsset -> + asset.format.conformsTo(Trait.LCP_PROTECTED) } } + } override fun contentProtection( authentication: LcpAuthenticating diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/EpubEncryption.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/EpubEncryption.kt deleted file mode 100644 index 86dd5fe7e7..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/EpubEncryption.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.publication.protection - -internal object EpubEncryption { - - const val ENC = "http://www.w3.org/2001/04/xmlenc#" - const val SIG = "http://www.w3.org/2000/09/xmldsig#" - const val COMP = "http://www.idpf.org/2016/encryption#compression" -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt index eb9d0c416c..af3e239bf0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt @@ -15,8 +15,8 @@ import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.format.Trait /** - * [ContentProtection] implementation used as a fallback by the Streamer to detect LCP DRM - * if it is not supported by the app. + * [ContentProtection] implementation used as a fallback by the PublicationFactory to detect DRMs + * not supported by the app. */ @InternalReadiumApi public class FallbackContentProtection : ContentProtection { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt index 25c8ebb2bf..7885d3d79a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt @@ -18,7 +18,6 @@ import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.ResourceFactory import org.readium.r2.shared.util.toUrl -import timber.log.Timber /** * Retrieves an [Asset] instance providing reading access to the resource(s) of an asset stored at @@ -69,7 +68,8 @@ public class AssetOpener( val resource = retrieveResource(url) .getOrElse { return Try.failure(it) } - val archive = archiveOpener.open(format, resource) + val archive = archiveOpener + .open(format, resource) .getOrElse { return when (it) { is ArchiveOpener.OpenError.Reading -> @@ -116,7 +116,6 @@ public class AssetOpener( ) } - Timber.d("sniffing asset") return assetSniffer.sniffOpen(resource, FormatHints(mediaType = mediaType)) .mapFailure { when (it) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt index f9394a0c5a..526779e6ec 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt @@ -9,20 +9,24 @@ package org.readium.r2.shared.util.asset import java.io.File import org.readium.r2.shared.util.Either import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.data.CachingContainer +import org.readium.r2.shared.util.data.CachingReadable import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.format.AdeptSniffer import org.readium.r2.shared.util.format.ArchiveSniffer +import org.readium.r2.shared.util.format.AudioSniffer import org.readium.r2.shared.util.format.BitmapSniffer import org.readium.r2.shared.util.format.BlobSniffer import org.readium.r2.shared.util.format.ContainerSniffer -import org.readium.r2.shared.util.format.ContentSniffer import org.readium.r2.shared.util.format.EpubSniffer +import org.readium.r2.shared.util.format.FileExtension import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatHints import org.readium.r2.shared.util.format.FormatHintsSniffer +import org.readium.r2.shared.util.format.FormatSniffer import org.readium.r2.shared.util.format.HtmlSniffer import org.readium.r2.shared.util.format.JsonSniffer import org.readium.r2.shared.util.format.LcpLicenseSniffer @@ -38,19 +42,20 @@ import org.readium.r2.shared.util.format.ZipSniffer import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.borrow +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.resource.mediaType import org.readium.r2.shared.util.tryRecover import org.readium.r2.shared.util.use import org.readium.r2.shared.util.zip.ZipArchiveOpener -import timber.log.Timber public class AssetSniffer( - private val contentSniffers: List = defaultContentSniffers, + private val formatSniffers: List = defaultFormatSniffers, private val archiveOpener: ArchiveOpener = ZipArchiveOpener() ) { public companion object { - public val defaultContentSniffers: List = listOf( + public val defaultFormatSniffers: List = listOf( ZipSniffer, RarSniffer, EpubSniffer, @@ -60,6 +65,7 @@ public class AssetSniffer( PdfSniffer, HtmlSniffer, BitmapSniffer, + AudioSniffer, JsonSniffer, OpdsSniffer, LcpLicenseSniffer, @@ -69,95 +75,119 @@ public class AssetSniffer( RwpmSniffer ) } - public suspend fun sniffOpen( - source: Resource, - hints: FormatHints = FormatHints() - ): Try = - sniff(null, Either.Left(source), hints) - - public suspend fun sniffOpen( - file: File, - hints: FormatHints = FormatHints() - ): Try = - sniff(null, Either.Left(FileResource(file)), hints) public suspend fun sniff( - source: Resource, + file: File, hints: FormatHints = FormatHints() ): Try = - sniffOpen(source.borrow(), hints).map { it.format } + FileResource(file).use { sniff(it, hints) } public suspend fun sniff( - file: File, + resource: Resource, hints: FormatHints = FormatHints() ): Try = - FileResource(file).use { sniff(it, hints) } + sniffOpen(resource.borrow(), hints).map { it.format } public suspend fun sniff( container: Container, hints: FormatHints = FormatHints() ): Try = - sniff(null, Either.Right(container), hints).map { it.format } + sniff(Either.Right(container), hints).map { it.format } + + public suspend fun sniffOpen( + resource: Resource, + hints: FormatHints = FormatHints() + ): Try { + val properties = resource.properties() + .getOrElse { return Try.failure(SniffError.Reading(it)) } + + val internalHints = FormatHints( + mediaType = properties.mediaType, + fileExtension = properties.filename + ?.substringAfterLast(".") + ?.let { FileExtension((it)) } + ) + + return sniff(Either.Left(resource), hints + internalHints) + } + + public suspend fun sniffOpen( + file: File, + hints: FormatHints = FormatHints() + ): Try = + sniff(Either.Left(FileResource(file)), hints) private suspend fun sniff( - format: Format?, source: Either>, + hints: FormatHints + ): Try { + val cachedSource: Either> = when (source) { + is Either.Left -> Either.Left(CachingReadable(source.value)) + is Either.Right -> Either.Right(CachingContainer(source.value)) + } + + val format = doSniff(null, cachedSource, hints) + .getOrElse { return Try.failure(SniffError.Reading(it)) } + + return format + ?.let { + Try.success( + when (source) { + is Either.Left -> ResourceAsset(it, source.value) + is Either.Right -> ContainerAsset(it, source.value) + } + ) + } ?: Try.failure(SniffError.NotRecognized) + } + + private suspend fun doSniff( + format: Format?, + source: Either>, hints: FormatHints, excludeHintsSniffer: FormatHintsSniffer? = null, excludeBlobSniffer: BlobSniffer? = null, excludeContainerSniffer: ContainerSniffer? = null - ): Try { - for (sniffer in contentSniffers) { + ): Try { + for (sniffer in formatSniffers) { sniffer .takeIf { it != excludeHintsSniffer } - .also { Timber.d("Trying $sniffer hints") } ?.sniffHints(format, hints) ?.takeIf { format == null || it.conformsTo(format) } ?.takeIf { it != format } - ?.let { return sniff(it, source, hints, excludeHintsSniffer = sniffer) } + ?.let { return doSniff(it, source, hints, excludeHintsSniffer = sniffer) } } when (source) { is Either.Left -> - for (sniffer in contentSniffers) { + for (sniffer in formatSniffers) { sniffer .takeIf { it != excludeBlobSniffer } - .also { Timber.d("Trying $sniffer blob") } ?.sniffBlob(format, source.value) - ?.getOrElse { return Try.failure(SniffError.Reading(it)) } + ?.getOrElse { return Try.failure(it) } ?.takeIf { format == null || it.conformsTo(format) } ?.takeIf { it != format } - ?.let { return sniff(it, source, hints, excludeBlobSniffer = sniffer) } + ?.let { return doSniff(it, source, hints, excludeBlobSniffer = sniffer) } } is Either.Right -> - for (sniffer in contentSniffers) { + for (sniffer in formatSniffers) { sniffer .takeIf { it != excludeContainerSniffer } - .also { Timber.d("Trying $sniffer container") } ?.sniffContainer(format, source.value) - ?.getOrElse { return Try.failure(SniffError.Reading(it)) } + ?.getOrElse { return Try.failure(it) } ?.takeIf { format == null || it.conformsTo(format) } ?.takeIf { it != format } - ?.let { return sniff(it, source, hints, excludeContainerSniffer = sniffer) } + ?.let { return doSniff(it, source, hints, excludeContainerSniffer = sniffer) } } } if (source is Either.Left) { tryOpenArchive(format, source.value) - .getOrElse { return Try.failure(SniffError.Reading(it)) } - ?.let { return sniff(it.format, Either.Right(it.container), hints) } - } - - format?.let { - val asset = when (source) { - is Either.Left -> ResourceAsset(it, source.value) - is Either.Right -> ContainerAsset(it, source.value) - } - return Try.success(asset) + .getOrElse { return Try.failure(it) } + ?.let { return doSniff(it.format, Either.Right(it.container), hints) } } - return Try.failure(SniffError.NotRecognized) + return Try.success(format) } private suspend fun tryOpenArchive( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt index d64be5e09b..8caade1a63 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt @@ -13,7 +13,6 @@ import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Manifest import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.encryption.encryption -import org.readium.r2.shared.publication.protection.EpubEncryption import org.readium.r2.shared.util.RelativeUrl import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url @@ -31,7 +30,7 @@ import org.readium.r2.shared.util.getOrDefault import org.readium.r2.shared.util.getOrElse /** Sniffs an HTML or XHTML document. */ -public object HtmlSniffer : ContentSniffer { +public object HtmlSniffer : FormatSniffer { override fun sniffHints( format: Format?, hints: FormatHints @@ -91,7 +90,7 @@ public object HtmlSniffer : ContentSniffer { } /** Sniffs an OPDS1 document. */ -public object OpdsSniffer : ContentSniffer { +public object OpdsSniffer : FormatSniffer { override fun sniffHints( format: Format?, @@ -221,7 +220,7 @@ public object OpdsSniffer : ContentSniffer { } /** Sniffs an LCP License Document. */ -public object LcpLicenseSniffer : ContentSniffer { +public object LcpLicenseSniffer : FormatSniffer { override fun sniffHints( format: Format?, hints: FormatHints @@ -257,7 +256,8 @@ public object LcpLicenseSniffer : ContentSniffer { } /** Sniffs a bitmap image. */ -public object BitmapSniffer : ContentSniffer { +public object BitmapSniffer : FormatSniffer { + override fun sniffHints( format: Format?, hints: FormatHints @@ -310,12 +310,29 @@ public object BitmapSniffer : ContentSniffer { ) { return Format(setOf(Trait.BITMAP, Trait.WEBP)) } + return format } } +/** Sniffs audio files. */ +public object AudioSniffer : FormatSniffer { + override fun sniffHints(format: Format?, hints: FormatHints): Format? { + if ( + hints.hasFileExtension( + "aac", "aiff", "alac", "flac", "m4a", "m4b", "mp3", + "ogg", "oga", "mogg", "opus", "wav", "webm" + ) + ) { + return Format(setOf(Trait.AUDIO)) + } + + return null + } +} + /** Sniffs a Readium Web Manifest. */ -public object RwpmSniffer : ContentSniffer { +public object RwpmSniffer : FormatSniffer { override fun sniffHints( format: Format?, hints: FormatHints @@ -369,7 +386,7 @@ public object RwpmSniffer : ContentSniffer { } /** Sniffs a Readium Web Publication, protected or not by LCP. */ -public object RpfSniffer : ContentSniffer { +public object RpfSniffer : FormatSniffer { override fun sniffHints( format: Format?, @@ -445,7 +462,7 @@ public object RpfSniffer : ContentSniffer { } /** Sniffs a W3C Web Publication Manifest. */ -public object W3cWpubSniffer : ContentSniffer { +public object W3cWpubSniffer : FormatSniffer { override suspend fun sniffBlob( format: Format?, @@ -483,7 +500,7 @@ public object W3cWpubSniffer : ContentSniffer { * * Reference: https://www.w3.org/publishing/epub3/epub-ocf.html#sec-zip-container-mime */ -public object EpubSniffer : ContentSniffer { +public object EpubSniffer : FormatSniffer { override fun sniffHints( format: Format?, hints: FormatHints @@ -528,7 +545,7 @@ public object EpubSniffer : ContentSniffer { * - https://www.w3.org/TR/lpf/ * - https://www.w3.org/TR/pub-manifest/ */ -public object LpfSniffer : ContentSniffer { +public object LpfSniffer : FormatSniffer { override fun sniffHints( format: Format?, hints: FormatHints @@ -584,7 +601,7 @@ public object LpfSniffer : ContentSniffer { * * At the moment, only hints are supported. */ -public object RarSniffer : ContentSniffer { +public object RarSniffer : FormatSniffer { override fun sniffHints( format: Format?, @@ -606,7 +623,7 @@ public object RarSniffer : ContentSniffer { /** * Sniffs a ZIP archive. */ -public object ZipSniffer : ContentSniffer { +public object ZipSniffer : FormatSniffer { override fun sniffHints( format: Format?, @@ -627,7 +644,7 @@ public object ZipSniffer : ContentSniffer { * * Reference: https://wiki.mobileread.com/wiki/CBR_and_CBZ */ -public object ArchiveSniffer : ContentSniffer { +public object ArchiveSniffer : FormatSniffer { /** * Authorized extensions for resources in a CBZ archive. @@ -738,7 +755,7 @@ public object ArchiveSniffer : ContentSniffer { * * Reference: https://www.loc.gov/preservation/digital/formats/fdd/fdd000123.shtml */ -public object PdfSniffer : ContentSniffer { +public object PdfSniffer : FormatSniffer { override fun sniffHints( format: Format?, hints: FormatHints @@ -772,7 +789,7 @@ public object PdfSniffer : ContentSniffer { } /** Sniffs a JSON document. */ -public object JsonSniffer : ContentSniffer { +public object JsonSniffer : FormatSniffer { override fun sniffHints( format: Format?, hints: FormatHints @@ -811,7 +828,7 @@ public object JsonSniffer : ContentSniffer { /** * Sniffs Adept protection on EPUBs. */ -public object AdeptSniffer : ContentSniffer { +public object AdeptSniffer : FormatSniffer { override suspend fun sniffContainer( format: Format?, @@ -847,7 +864,7 @@ public object AdeptSniffer : ContentSniffer { /** * Sniffs LCP protected packages. */ -public object LcpSniffer : ContentSniffer { +public object LcpSniffer : FormatSniffer { override suspend fun sniffContainer( format: Format?, @@ -934,3 +951,8 @@ private suspend fun Readable.containsJsonKeys( ) return Try.success(json.keys().asSequence().toSet().containsAll(keys.toList())) } + +private object EpubEncryption { + const val ENC = "http://www.w3.org/2001/04/xmlenc#" + const val SIG = "http://www.w3.org/2000/09/xmldsig#" +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/ContentSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatSniffer.kt similarity index 97% rename from readium/shared/src/main/java/org/readium/r2/shared/util/format/ContentSniffer.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatSniffer.kt index 91b24ac96d..e39e9bc7a1 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/ContentSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatSniffer.kt @@ -44,7 +44,7 @@ public interface ContainerSniffer { ): Try } -public interface ContentSniffer : +public interface FormatSniffer : FormatHintsSniffer, BlobSniffer, ContainerSniffer { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt index 5450839442..a2f6c7b2ed 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipContainer.kt @@ -28,6 +28,7 @@ import org.readium.r2.shared.util.io.CountingInputStream import org.readium.r2.shared.util.resource.ArchiveProperties import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.archive +import org.readium.r2.shared.util.resource.filename import org.readium.r2.shared.util.toUrl internal class FileZipContainer( @@ -43,6 +44,7 @@ internal class FileZipContainer( override suspend fun properties(): Try = Try.success( Resource.Properties { + filename = url.filename archive = ArchiveProperties( entryLength = compressedLength ?: length().getOrElse { return Try.failure(it) }, diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt index 1d58b18788..eb0ae2dd9d 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt @@ -563,7 +563,7 @@ class AssetSnifferTest { assertEquals( Format.JSON_PROBLEM_DETAILS, sniffer.sniff( - source = StringResource("""{"title": "Message"}"""), + resource = StringResource("""{"title": "Message"}"""), hints = FormatHints(mediaType = MediaType("application/problem+json")!!) ).checkSuccess() ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt index ab63caafa8..684f3e0543 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt @@ -158,24 +158,24 @@ public class PublicationFactory( private suspend fun parse( publicationAsset: Asset, warnings: WarningLogger? - ): Try { + ): Try { for (parser in parsers) { val result = parser.parse(publicationAsset, warnings) if ( result is Try.Success || - result is Try.Failure && result.value !is PublicationParser.Error.FormatNotSupported + result is Try.Failure && result.value !is PublicationParser.ParseError.FormatNotSupported ) { return result } } - return Try.failure(PublicationParser.Error.FormatNotSupported()) + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } - private fun wrapParserException(e: PublicationParser.Error): OpenError = + private fun wrapParserException(e: PublicationParser.ParseError): OpenError = when (e) { - is PublicationParser.Error.FormatNotSupported -> + is PublicationParser.ParseError.FormatNotSupported -> OpenError.FormatNotSupported(DebugError("Cannot find a parser for this asset.")) - is PublicationParser.Error.Reading -> + is PublicationParser.ParseError.Reading -> OpenError.Reading(e.cause) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt index 7887afba3a..70c37b5994 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt @@ -11,11 +11,17 @@ package org.readium.r2.streamer.extensions import java.io.File import org.readium.r2.shared.extensions.addPrefix +import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.asset.AssetSniffer +import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.format.FileExtension +import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.SingleResourceContainer +import org.readium.r2.shared.util.use internal fun Iterable.guessTitle(): String? { val firstEntry = firstOrNull() ?: return null @@ -52,3 +58,31 @@ internal fun Resource.toContainer( this ) } + +internal suspend fun AssetSniffer.sniffContainerEntries( + container: Container, + filter: (Url) -> Boolean +): Try, ReadError> = + container + .filter(filter) + .fold, ReadError>>(Try.success(emptyMap())) { acc, url -> + when (acc) { + is Try.Failure -> + acc + + is Try.Success -> + container[url]!!.use { resource -> + sniff(resource).fold( + onSuccess = { + Try.success(acc.value + (url to it)) + }, + onFailure = { + when (it) { + SniffError.NotRecognized -> acc + is SniffError.Reading -> Try.failure(it.cause) + } + } + ) + } + } + } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt index b18b2123c1..f7e6dd6eb6 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/PublicationParser.kt @@ -27,17 +27,17 @@ public interface PublicationParser { public suspend fun parse( asset: Asset, warnings: WarningLogger? = null - ): Try + ): Try - public sealed class Error( + public sealed class ParseError( public override val message: String, public override val cause: org.readium.r2.shared.util.Error? ) : org.readium.r2.shared.util.Error { public class FormatNotSupported : - Error("Asset format not supported.", null) + ParseError("Asset format not supported.", null) public class Reading(override val cause: org.readium.r2.shared.util.data.ReadError) : - Error("An error occurred while trying to read asset.", cause) + ParseError("An error occurred while trying to read asset.", cause) } } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index ff82ccfb36..da50d524a6 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -18,15 +18,15 @@ import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.ResourceAsset -import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatRegistry +import org.readium.r2.shared.util.format.Trait import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger -import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs +import org.readium.r2.streamer.extensions.sniffContainerEntries import org.readium.r2.streamer.extensions.toContainer import org.readium.r2.streamer.parser.PublicationParser @@ -44,9 +44,9 @@ public class AudioParser( override suspend fun parse( asset: Asset, warnings: WarningLogger? - ): Try { - if (!asset.format.conformsTo(Format.ZAB) && formatRegistry[asset.format]?.mediaType?.isAudio != true) { - return Try.failure(PublicationParser.Error.FormatNotSupported()) + ): Try { + if (!asset.format.conformsTo(Trait.AUDIOBOOK) && !asset.format.conformsTo(Trait.AUDIO)) { + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } val container = when (asset) { @@ -56,20 +56,23 @@ public class AudioParser( asset.container } - val readingOrder = - if (asset.format.conformsTo(Format.ZAB)) { + val entryFormats: Map = assetSniffer + .sniffContainerEntries(container) { !it.isHiddenOrThumbs } + .getOrElse { return Try.failure(PublicationParser.ParseError.Reading(it)) } + + val readingOrderWithFormat = + if (asset.format.conformsTo(Trait.AUDIOBOOK)) { container - .filter { zabCanContain(it) } - .sortedBy { it.toString() } + .mapNotNull { url -> entryFormats[url]?.let { url to it } } + .filter { it.second.conformsTo(Trait.AUDIO) } + .sortedBy { it.first.toString() } } else { - listOfNotNull( - container.entries.firstOrNull() - ) + listOfNotNull(container.first() to asset.format) } - if (readingOrder.isEmpty()) { + if (readingOrderWithFormat.isEmpty()) { return Try.failure( - PublicationParser.Error.Reading( + PublicationParser.ParseError.Reading( ReadError.Decoding( DebugError("No audio file found in the publication.") ) @@ -77,19 +80,8 @@ public class AudioParser( ) } - val readingOrderLinks = readingOrder.map { url -> - val mediaType = container[url]!!.use { resource -> - assetSniffer.sniff(resource) - .map { formatRegistry[it]?.mediaType } - .getOrElse { error -> - when (error) { - SniffError.NotRecognized -> - null - is SniffError.Reading -> - return Try.failure(PublicationParser.Error.Reading(error.cause)) - } - } - } + val readingOrderLinks = readingOrderWithFormat.map { (url, format) -> + val mediaType = formatRegistry[format]?.mediaType Link(href = url, mediaType = mediaType) } @@ -111,12 +103,4 @@ public class AudioParser( return Try.success(publicationBuilder) } - - private fun zabCanContain(url: Url): Boolean = - url.extension?.lowercase() in audioExtensions && !url.isHiddenOrThumbs - - private val audioExtensions = listOf( - "aac", "aiff", "alac", "flac", "m4a", "m4b", "mp3", - "ogg", "oga", "mogg", "opus", "wav", "webm" - ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 88581fba1a..4ada5b3daa 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -50,16 +50,16 @@ public class EpubParser( override suspend fun parse( asset: Asset, warnings: WarningLogger? - ): Try { + ): Try { if (asset !is ContainerAsset || !asset.format.conformsTo(Format.EPUB)) { - return Try.failure(PublicationParser.Error.FormatNotSupported()) + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } val opfPath = getRootFilePath(asset.container) .getOrElse { return Try.failure(it) } val opfResource = asset.container[opfPath] ?: return Try.failure( - PublicationParser.Error.Reading( + PublicationParser.ParseError.Reading( ReadError.Decoding( DebugError("Missing OPF file.") ) @@ -68,12 +68,12 @@ public class EpubParser( val opfXmlDocument = opfResource.use { resource -> resource.readDecodeOrElse( decode = { it.decodeXml() }, - recover = { return Try.failure(PublicationParser.Error.Reading(it)) } + recover = { return Try.failure(PublicationParser.ParseError.Reading(it)) } ) } val packageDocument = PackageDocument.parse(opfXmlDocument, opfPath) ?: return Try.failure( - PublicationParser.Error.Reading( + PublicationParser.ParseError.Reading( ReadError.Decoding( DebugError("Invalid OPF file.") ) @@ -112,12 +112,12 @@ public class EpubParser( return Try.success(builder) } - private suspend fun getRootFilePath(container: Container): Try { + private suspend fun getRootFilePath(container: Container): Try { val containerXmlUrl = Url("META-INF/container.xml")!! val containerXmlResource = container[containerXmlUrl] ?: return Try.failure( - PublicationParser.Error.Reading( + PublicationParser.ParseError.Reading( ReadError.Decoding("container.xml not found.") ) ) @@ -125,7 +125,7 @@ public class EpubParser( return containerXmlResource .readDecodeOrElse( decode = { it.decodeXml() }, - recover = { return Try.failure(PublicationParser.Error.Reading(it)) } + recover = { return Try.failure(PublicationParser.ParseError.Reading(it)) } ) .getFirst("rootfiles", Namespaces.OPC) ?.getFirst("rootfile", Namespaces.OPC) @@ -133,7 +133,7 @@ public class EpubParser( ?.let { Url.fromEpubHref(it) } ?.let { Try.success(it) } ?: Try.failure( - PublicationParser.Error.Reading( + PublicationParser.ParseError.Reading( ReadError.Decoding("Cannot successfully parse OPF.") ) ) diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index 2fc34f44b2..01f1074714 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -19,16 +19,16 @@ import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.ResourceAsset -import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatRegistry +import org.readium.r2.shared.util.format.Trait import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.use import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs +import org.readium.r2.streamer.extensions.sniffContainerEntries import org.readium.r2.streamer.extensions.toContainer import org.readium.r2.streamer.parser.PublicationParser @@ -46,9 +46,9 @@ public class ImageParser( override suspend fun parse( asset: Asset, warnings: WarningLogger? - ): Try { - if (!asset.format.conformsTo(Format.CBZ) && formatRegistry[asset.format]?.mediaType?.isBitmap != true) { - return Try.failure(PublicationParser.Error.FormatNotSupported()) + ): Try { + if (!asset.format.conformsTo(Trait.COMICS) && !asset.format.conformsTo(Trait.BITMAP)) { + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } val container = when (asset) { @@ -58,18 +58,23 @@ public class ImageParser( asset.container } - val readingOrder = - if (asset.format.conformsTo(Format.CBZ)) { - (container) - .filter { cbzCanContain(it) } - .sortedBy { it.toString() } + val entryFormats: Map = assetSniffer + .sniffContainerEntries(container) { !it.isHiddenOrThumbs } + .getOrElse { return Try.failure(PublicationParser.ParseError.Reading(it)) } + + val readingOrderWithFormat = + if (asset.format.conformsTo(Trait.COMICS)) { + container + .mapNotNull { url -> entryFormats[url]?.let { url to it } } + .filter { it.second.conformsTo(Trait.BITMAP) } + .sortedBy { it.first.toString() } } else { - listOfNotNull(container.firstOrNull()) + listOfNotNull(container.first() to asset.format) } - if (readingOrder.isEmpty()) { + if (readingOrderWithFormat.isEmpty()) { return Try.failure( - PublicationParser.Error.Reading( + PublicationParser.ParseError.Reading( ReadError.Decoding( DebugError("No bitmap found in the publication.") ) @@ -77,19 +82,8 @@ public class ImageParser( ) } - val readingOrderLinks = readingOrder.map { url -> - val mediaType = container[url]!!.use { resource -> - assetSniffer.sniff(resource) - .map { formatRegistry[it]?.mediaType } - .getOrElse { error -> - when (error) { - SniffError.NotRecognized -> - null - is SniffError.Reading -> - return Try.failure(PublicationParser.Error.Reading(error.cause)) - } - } - } + val readingOrderLinks = readingOrderWithFormat.map { (url, format) -> + val mediaType = formatRegistry[format]?.mediaType Link(href = url, mediaType = mediaType) }.toMutableList() @@ -116,12 +110,4 @@ public class ImageParser( return Try.success(publicationBuilder) } - - private fun cbzCanContain(url: Url): Boolean = - url.extension?.lowercase() in bitmapExtensions && !url.isHiddenOrThumbs - - private val bitmapExtensions = listOf( - "bmp", "dib", "gif", "jif", "jfi", "jfif", "jpg", "jpeg", - "png", "tif", "tiff", "webp" - ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index 0d5070136e..a7dea73d6d 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -40,9 +40,9 @@ public class PdfParser( override suspend fun parse( asset: Asset, warnings: WarningLogger? - ): Try { + ): Try { if (asset !is ResourceAsset || !asset.format.conformsTo(Format.PDF)) { - return Try.failure(PublicationParser.Error.FormatNotSupported()) + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } val container = asset.resource @@ -52,7 +52,7 @@ public class PdfParser( .first() val document = pdfFactory.open(container[url]!!, password = null) - .getOrElse { return Try.failure(PublicationParser.Error.Reading(it)) } + .getOrElse { return Try.failure(PublicationParser.ParseError.Reading(it)) } val tableOfContents = document.outline.toLinks(url) val manifest = Manifest( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt index 0182a8f55a..c993144ac8 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/readium/ReadiumWebPubParser.kt @@ -50,7 +50,7 @@ public class ReadiumWebPubParser( override suspend fun parse( asset: Asset, warnings: WarningLogger? - ): Try { + ): Try { if ( asset is ResourceAsset && ( @@ -66,12 +66,12 @@ public class ReadiumWebPubParser( } if (asset !is ContainerAsset || !asset.format.conformsTo(Trait.RPF)) { - return Try.failure(PublicationParser.Error.FormatNotSupported()) + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } val manifestResource = asset.container[Url("manifest.json")!!] ?: return Try.failure( - PublicationParser.Error.Reading( + PublicationParser.ParseError.Reading( ReadError.Decoding( DebugError("Missing manifest.") ) @@ -81,7 +81,7 @@ public class ReadiumWebPubParser( val manifest = manifestResource .readDecodeOrElse( decode = { it.decodeRwpm() }, - recover = { return Try.failure(PublicationParser.Error.Reading(it)) } + recover = { return Try.failure(PublicationParser.ParseError.Reading(it)) } ) // Checks the requirements from the LCPDF specification. @@ -91,7 +91,7 @@ public class ReadiumWebPubParser( (readingOrder.isEmpty() || !readingOrder.all { MediaType.PDF.matches(it.mediaType) }) ) { return Try.failure( - PublicationParser.Error.Reading( + PublicationParser.ParseError.Reading( ReadError.Decoding("Invalid LCP Protected PDF.") ) ) @@ -123,11 +123,11 @@ public class ReadiumWebPubParser( return Try.success(publicationBuilder) } - private suspend fun createPackage(asset: ResourceAsset): Try { + private suspend fun createPackage(asset: ResourceAsset): Try { val manifest = asset.resource .readDecodeOrElse( decode = { it.decodeRwpm() }, - recover = { return Try.failure(PublicationParser.Error.Reading(it)) } + recover = { return Try.failure(PublicationParser.ParseError.Reading(it)) } ) val baseUrl = manifest.linkWithRel("self")?.href?.resolve() @@ -136,14 +136,14 @@ public class ReadiumWebPubParser( } else { if (baseUrl !is AbsoluteUrl) { return Try.failure( - PublicationParser.Error.Reading( + PublicationParser.ParseError.Reading( ReadError.Decoding("Self link is not absolute.") ) ) } if (!baseUrl.isHttp) { return Try.failure( - PublicationParser.Error.Reading( + PublicationParser.ParseError.Reading( ReadError.Decoding("Self link doesn't use the HTTP(S) scheme.") ) ) From c68016e018de13a2a7344e2c1987bf362a794381 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Tue, 19 Dec 2023 08:03:34 +0100 Subject: [PATCH 82/86] Fix sniffing --- .../r2/shared/util/asset/AssetSniffer.kt | 95 ++++--------------- .../r2/shared/util/format/DefaultSniffers.kt | 85 ++++++++++------- .../readium/r2/shared/util/format/Format.kt | 3 + .../r2/shared/util/format/FormatHints.kt | 1 - .../r2/shared/util/format/FormatSniffer.kt | 33 ++++++- .../r2/streamer/extensions/Container.kt | 2 +- 6 files changed, 109 insertions(+), 110 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt index 526779e6ec..4bd87a6d96 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt @@ -15,30 +15,11 @@ import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.file.FileResource -import org.readium.r2.shared.util.format.AdeptSniffer -import org.readium.r2.shared.util.format.ArchiveSniffer -import org.readium.r2.shared.util.format.AudioSniffer -import org.readium.r2.shared.util.format.BitmapSniffer -import org.readium.r2.shared.util.format.BlobSniffer -import org.readium.r2.shared.util.format.ContainerSniffer -import org.readium.r2.shared.util.format.EpubSniffer +import org.readium.r2.shared.util.format.DefaultFormatSniffer import org.readium.r2.shared.util.format.FileExtension import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatHints -import org.readium.r2.shared.util.format.FormatHintsSniffer import org.readium.r2.shared.util.format.FormatSniffer -import org.readium.r2.shared.util.format.HtmlSniffer -import org.readium.r2.shared.util.format.JsonSniffer -import org.readium.r2.shared.util.format.LcpLicenseSniffer -import org.readium.r2.shared.util.format.LcpSniffer -import org.readium.r2.shared.util.format.LpfSniffer -import org.readium.r2.shared.util.format.OpdsSniffer -import org.readium.r2.shared.util.format.PdfSniffer -import org.readium.r2.shared.util.format.RarSniffer -import org.readium.r2.shared.util.format.RpfSniffer -import org.readium.r2.shared.util.format.RwpmSniffer -import org.readium.r2.shared.util.format.W3cWpubSniffer -import org.readium.r2.shared.util.format.ZipSniffer import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.borrow @@ -49,33 +30,9 @@ import org.readium.r2.shared.util.use import org.readium.r2.shared.util.zip.ZipArchiveOpener public class AssetSniffer( - private val formatSniffers: List = defaultFormatSniffers, + private val formatSniffer: FormatSniffer = DefaultFormatSniffer(), private val archiveOpener: ArchiveOpener = ZipArchiveOpener() ) { - - public companion object { - - public val defaultFormatSniffers: List = listOf( - ZipSniffer, - RarSniffer, - EpubSniffer, - LpfSniffer, - ArchiveSniffer, - RpfSniffer, - PdfSniffer, - HtmlSniffer, - BitmapSniffer, - AudioSniffer, - JsonSniffer, - OpdsSniffer, - LcpLicenseSniffer, - LcpSniffer, - AdeptSniffer, - W3cWpubSniffer, - RwpmSniffer - ) - } - public suspend fun sniff( file: File, hints: FormatHints = FormatHints() @@ -143,42 +100,30 @@ public class AssetSniffer( private suspend fun doSniff( format: Format?, source: Either>, - hints: FormatHints, - excludeHintsSniffer: FormatHintsSniffer? = null, - excludeBlobSniffer: BlobSniffer? = null, - excludeContainerSniffer: ContainerSniffer? = null + hints: FormatHints ): Try { - for (sniffer in formatSniffers) { - sniffer - .takeIf { it != excludeHintsSniffer } - ?.sniffHints(format, hints) - ?.takeIf { format == null || it.conformsTo(format) } - ?.takeIf { it != format } - ?.let { return doSniff(it, source, hints, excludeHintsSniffer = sniffer) } - } + formatSniffer + .sniffHints(format, hints) + ?.takeIf { format == null || it.conformsTo(format) } + ?.takeIf { it != format } + ?.let { return doSniff(it, source, hints) } when (source) { is Either.Left -> - for (sniffer in formatSniffers) { - sniffer - .takeIf { it != excludeBlobSniffer } - ?.sniffBlob(format, source.value) - ?.getOrElse { return Try.failure(it) } - ?.takeIf { format == null || it.conformsTo(format) } - ?.takeIf { it != format } - ?.let { return doSniff(it, source, hints, excludeBlobSniffer = sniffer) } - } + formatSniffer + .sniffBlob(format, source.value) + .getOrElse { return Try.failure(it) } + ?.takeIf { format == null || it.conformsTo(format) } + ?.takeIf { it != format } + ?.let { return doSniff(it, source, hints) } is Either.Right -> - for (sniffer in formatSniffers) { - sniffer - .takeIf { it != excludeContainerSniffer } - ?.sniffContainer(format, source.value) - ?.getOrElse { return Try.failure(it) } - ?.takeIf { format == null || it.conformsTo(format) } - ?.takeIf { it != format } - ?.let { return doSniff(it, source, hints, excludeContainerSniffer = sniffer) } - } + formatSniffer + .sniffContainer(format, source.value) + .getOrElse { return Try.failure(it) } + ?.takeIf { format == null || it.conformsTo(format) } + ?.takeIf { it != format } + ?.let { return doSniff(it, source, hints) } } if (source is Either.Left) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt index 8caade1a63..d51175a396 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt @@ -29,6 +29,27 @@ import org.readium.r2.shared.util.format.Format.Companion.orEmpty import org.readium.r2.shared.util.getOrDefault import org.readium.r2.shared.util.getOrElse +public class DefaultFormatSniffer : + FormatSniffer by CompositeFormatSniffer( + ZipSniffer, + RarSniffer, + EpubSniffer, + LpfSniffer, + ArchiveSniffer, + RpfSniffer, + PdfSniffer, + HtmlSniffer, + BitmapSniffer, + AudioSniffer, + JsonSniffer, + OpdsSniffer, + LcpLicenseSniffer, + LcpSniffer, + AdeptSniffer, + W3cWpubSniffer, + RwpmSniffer + ) + /** Sniffs an HTML or XHTML document. */ public object HtmlSniffer : FormatSniffer { override fun sniffHints( @@ -39,14 +60,14 @@ public object HtmlSniffer : FormatSniffer { hints.hasFileExtension("htm", "html") || hints.hasMediaType("text/html") ) { - return Format.HTML + return format.orEmpty() + Format.HTML } if ( hints.hasFileExtension("xht", "xhtml") || hints.hasMediaType("application/xhtml+xml") ) { - return Format.XHTML + return format.orEmpty() + Format.XHTML } return format @@ -103,16 +124,16 @@ public object OpdsSniffer : FormatSniffer { private fun sniffHintsXml(format: Format?, hints: FormatHints): Format? { // OPDS 1 if (hints.hasMediaType("application/atom+xml;type=entry;profile=opds-catalog")) { - return Format.OPDS1_ENTRY + return format.orEmpty() + Format.OPDS1_ENTRY } if (hints.hasMediaType("application/atom+xml;profile=opds-catalog;kind=navigation")) { - return Format.OPDS1_NAVIGATION_FEED + return format.orEmpty() + Format.OPDS1_NAVIGATION_FEED } if (hints.hasMediaType("application/atom+xml;profile=opds-catalog;kind=acquisition")) { - return Format.OPDS1_ACQUISITION_FEED + return format.orEmpty() + Format.OPDS1_ACQUISITION_FEED } if (hints.hasMediaType("application/atom+xml;profile=opds-catalog")) { - return Format.OPDS1_CATALOG + return format.orEmpty() + Format.OPDS1_CATALOG } return format @@ -121,10 +142,10 @@ public object OpdsSniffer : FormatSniffer { private fun sniffHintsJson(format: Format?, hints: FormatHints): Format? { // OPDS 2 if (hints.hasMediaType("application/opds+json")) { - return Format.OPDS2_CATALOG + return format.orEmpty() + Format.OPDS2_CATALOG } if (hints.hasMediaType("application/opds-publication+json")) { - return Format.OPDS2_PUBLICATION + return format.orEmpty() + Format.OPDS2_PUBLICATION } // OPDS Authentication Document. @@ -132,7 +153,7 @@ public object OpdsSniffer : FormatSniffer { hints.hasMediaType("application/opds-authentication+json") || hints.hasMediaType("application/vnd.opds.authentication.v1.0+json") ) { - return Format.OPDS_AUTHENTICATION + return format.orEmpty() + Format.OPDS_AUTHENTICATION } return format @@ -229,7 +250,7 @@ public object LcpLicenseSniffer : FormatSniffer { hints.hasFileExtension("lcpl") || hints.hasMediaType("application/vnd.readium.lcp.license.v1.0+json") ) { - return Format.LCP_LICENSE_DOCUMENT + return format.orEmpty() + Format.LCP_LICENSE_DOCUMENT } return format @@ -266,49 +287,49 @@ public object BitmapSniffer : FormatSniffer { hints.hasFileExtension("avif") || hints.hasMediaType("image/avif") ) { - return Format(setOf(Trait.BITMAP, Trait.AVIF)) + return format.orEmpty() + Format(setOf(Trait.BITMAP, Trait.AVIF)) } if ( hints.hasFileExtension("bmp", "dib") || hints.hasMediaType("image/bmp", "image/x-bmp") ) { - return Format(setOf(Trait.BITMAP, Trait.BMP)) + return format.orEmpty() + Format(setOf(Trait.BITMAP, Trait.BMP)) } if ( hints.hasFileExtension("gif") || hints.hasMediaType("image/gif") ) { - return Format(setOf(Trait.BITMAP, Trait.GIF)) + return format.orEmpty() + Format(setOf(Trait.BITMAP, Trait.GIF)) } if ( hints.hasFileExtension("jpg", "jpeg", "jpe", "jif", "jfif", "jfi") || hints.hasMediaType("image/jpeg") ) { - return Format(setOf(Trait.BITMAP, Trait.JPEG)) + return format.orEmpty() + Format(setOf(Trait.BITMAP, Trait.JPEG)) } if ( hints.hasFileExtension("jxl") || hints.hasMediaType("image/jxl") ) { - return Format(setOf(Trait.BITMAP, Trait.JXL)) + return format.orEmpty() + Format(setOf(Trait.BITMAP, Trait.JXL)) } if ( hints.hasFileExtension("png") || hints.hasMediaType("image/png") ) { - return Format(setOf(Trait.BITMAP, Trait.PNG)) + return format.orEmpty() + Format(setOf(Trait.BITMAP, Trait.PNG)) } if ( hints.hasFileExtension("tiff", "tif") || hints.hasMediaType("image/tiff", "image/tiff-fx") ) { - return Format(setOf(Trait.BITMAP, Trait.TIFF)) + return format.orEmpty() + Format(setOf(Trait.BITMAP, Trait.TIFF)) } if ( hints.hasFileExtension("webp") || hints.hasMediaType("image/webp") ) { - return Format(setOf(Trait.BITMAP, Trait.WEBP)) + return format.orEmpty() + Format(setOf(Trait.BITMAP, Trait.WEBP)) } return format @@ -324,10 +345,10 @@ public object AudioSniffer : FormatSniffer { "ogg", "oga", "mogg", "opus", "wav", "webm" ) ) { - return Format(setOf(Trait.AUDIO)) + return format.orEmpty() + Format(setOf(Trait.AUDIO)) } - return null + return format } } @@ -338,15 +359,15 @@ public object RwpmSniffer : FormatSniffer { hints: FormatHints ): Format? { if (hints.hasMediaType("application/audiobook+json")) { - return Format.READIUM_AUDIOBOOK_MANIFEST + return format.orEmpty() + Format.READIUM_AUDIOBOOK_MANIFEST } if (hints.hasMediaType("application/divina+json")) { - return Format.READIUM_COMICS_MANIFEST + return format.orEmpty() + Format.READIUM_COMICS_MANIFEST } if (hints.hasMediaType("application/webpub+json")) { - return Format.READIUM_WEBPUB_MANIFEST + return format.orEmpty() + Format.READIUM_WEBPUB_MANIFEST } return format @@ -396,34 +417,34 @@ public object RpfSniffer : FormatSniffer { hints.hasFileExtension("audiobook") || hints.hasMediaType("application/audiobook+zip") ) { - return Format.READIUM_AUDIOBOOK + return format.orEmpty() + Format.READIUM_AUDIOBOOK } if ( hints.hasFileExtension("divina") || hints.hasMediaType("application/divina+zip") ) { - return Format.READIUM_COMICS + return format.orEmpty() + Format.READIUM_COMICS } if ( hints.hasFileExtension("webpub") || hints.hasMediaType("application/webpub+zip") ) { - return Format.READIUM_WEBPUB + return format.orEmpty() + Format.READIUM_WEBPUB } if ( hints.hasFileExtension("lcpa") || hints.hasMediaType("application/audiobook+lcp") ) { - return Format.READIUM_AUDIOBOOK + Trait.LCP_PROTECTED + return format.orEmpty() + Format.READIUM_AUDIOBOOK + Trait.LCP_PROTECTED } if ( hints.hasFileExtension("lcpdf") || hints.hasMediaType("application/pdf+lcp") ) { - return Format.READIUM_PDF + Trait.LCP_PROTECTED + return format.orEmpty() + Format.READIUM_PDF + Trait.LCP_PROTECTED } return format @@ -509,7 +530,7 @@ public object EpubSniffer : FormatSniffer { hints.hasFileExtension("epub") || hints.hasMediaType("application/epub+zip") ) { - return Format.EPUB + return format.orEmpty() + Format.EPUB } return format @@ -554,7 +575,7 @@ public object LpfSniffer : FormatSniffer { hints.hasFileExtension("lpf") || hints.hasMediaType("application/lpf+zip") ) { - return Format(setOf(Trait.ZIP, Trait.LPF)) + return format.orEmpty() + Trait.ZIP + Trait.LPF } return format @@ -613,7 +634,7 @@ public object RarSniffer : FormatSniffer { hints.hasMediaType("application/x-rar") || hints.hasMediaType("application/x-rar-compressed") ) { - return Format.RAR + return format.orEmpty() + Format.RAR } return format @@ -632,7 +653,7 @@ public object ZipSniffer : FormatSniffer { if (hints.hasMediaType("application/zip") || hints.hasFileExtension("zip") ) { - return Format.ZIP + return format.orEmpty() + Format.ZIP } return format diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt index f0654cc7fc..ca920a5a5a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/Format.kt @@ -72,6 +72,9 @@ public value class Format(private val traits: Set) { public operator fun plus(trait: Trait): Format = Format(traits + trait) + public operator fun plus(other: Format): Format = + Format(traits + other.traits) + public operator fun minus(trait: Trait): Format = Format(traits - trait) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt index 368672cdd4..709981f753 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt @@ -13,7 +13,6 @@ import org.readium.r2.shared.util.mediatype.MediaType * Bundle of media type and file extension hints for the [FormatHintsSniffer]. */ public data class FormatHints( - val format: Format? = null, val mediaTypes: List = emptyList(), val fileExtensions: List = emptyList() ) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatSniffer.kt index e39e9bc7a1..e474af2c09 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatSniffer.kt @@ -53,7 +53,7 @@ public interface FormatSniffer : format: Format?, hints: FormatHints ): Format? = - null + format public override suspend fun sniffBlob( format: Format?, @@ -67,3 +67,34 @@ public interface FormatSniffer : ): Try = Try.success(format) } + +public class CompositeFormatSniffer( + private val sniffers: List +) : FormatSniffer { + + public constructor(vararg sniffers: FormatSniffer) : this(sniffers.toList()) + + override fun sniffHints(format: Format?, hints: FormatHints): Format? = + sniffers.fold(format) { acc, sniffer -> + sniffer.sniffHints(acc, hints) + } + + override suspend fun sniffBlob(format: Format?, source: Readable): Try = + sniffers.fold(Try.success(format)) { acc: Try, sniffer -> + when (acc) { + is Try.Failure -> acc + is Try.Success -> sniffer.sniffBlob(acc.value, source) + } + } + + override suspend fun sniffContainer( + format: Format?, + container: Container + ): Try = + sniffers.fold(Try.success(format)) { acc: Try, sniffer -> + when (acc) { + is Try.Failure -> acc + is Try.Success -> sniffer.sniffContainer(acc.value, container) + } + } +} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt index 70c37b5994..19726b743a 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt @@ -65,7 +65,7 @@ internal suspend fun AssetSniffer.sniffContainerEntries( ): Try, ReadError> = container .filter(filter) - .fold, ReadError>>(Try.success(emptyMap())) { acc, url -> + .fold(Try.success(emptyMap())) { acc: Try, ReadError>, url -> when (acc) { is Try.Failure -> acc From 153736a33d3befeaf00a475d8a58a42f2786b5d4 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Tue, 19 Dec 2023 08:28:04 +0100 Subject: [PATCH 83/86] Use FileExtension in Url.extension --- .../java/org/readium/r2/shared/util/Url.kt | 8 +- .../r2/shared/util/asset/AssetSniffer.kt | 97 +++++++++++++++---- .../android/AndroidDownloadManager.kt | 2 +- .../r2/shared/util/format/DefaultSniffers.kt | 2 +- .../r2/shared/util/format/FormatHints.kt | 1 + .../r2/shared/util/format/FormatRegistry.kt | 5 +- .../org/readium/r2/shared/util/UrlTest.kt | 10 +- .../shared/util/mediatype/AssetSnifferTest.kt | 2 +- .../util/mediatype/FormatRegistryTest.kt | 2 +- .../r2/streamer/extensions/Container.kt | 4 +- 10 files changed, 96 insertions(+), 37 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt index bd67a5960a..9c01bc16a0 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt @@ -83,9 +83,10 @@ public sealed class Url : Parcelable { /** * Extension of the filename portion of the URL path. */ - public val extension: String? + public val extension: FileExtension? get() = filename?.substringAfterLast('.', "") ?.takeIf { it.isNotEmpty() } + ?.let { FileExtension(it) } /** * Represents a list of query parameters in a URL. @@ -354,3 +355,8 @@ private fun Uri.addFileAuthority(): Uri = private fun String.isValidUrl(): Boolean = // Uri.parse doesn't really validate the URL, it could contain invalid characters. isNotBlank() && tryOrNull { URI(this) } != null + +@JvmInline +public value class FileExtension( + public val value: String +) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt index 4bd87a6d96..18f7e31f3a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt @@ -8,6 +8,7 @@ package org.readium.r2.shared.util.asset import java.io.File import org.readium.r2.shared.util.Either +import org.readium.r2.shared.util.FileExtension import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.CachingContainer import org.readium.r2.shared.util.data.CachingReadable @@ -15,11 +16,29 @@ import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.file.FileResource -import org.readium.r2.shared.util.format.DefaultFormatSniffer -import org.readium.r2.shared.util.format.FileExtension +import org.readium.r2.shared.util.format.AdeptSniffer +import org.readium.r2.shared.util.format.ArchiveSniffer +import org.readium.r2.shared.util.format.AudioSniffer +import org.readium.r2.shared.util.format.BitmapSniffer +import org.readium.r2.shared.util.format.BlobSniffer +import org.readium.r2.shared.util.format.ContainerSniffer +import org.readium.r2.shared.util.format.EpubSniffer import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatHints +import org.readium.r2.shared.util.format.FormatHintsSniffer import org.readium.r2.shared.util.format.FormatSniffer +import org.readium.r2.shared.util.format.HtmlSniffer +import org.readium.r2.shared.util.format.JsonSniffer +import org.readium.r2.shared.util.format.LcpLicenseSniffer +import org.readium.r2.shared.util.format.LcpSniffer +import org.readium.r2.shared.util.format.LpfSniffer +import org.readium.r2.shared.util.format.OpdsSniffer +import org.readium.r2.shared.util.format.PdfSniffer +import org.readium.r2.shared.util.format.RarSniffer +import org.readium.r2.shared.util.format.RpfSniffer +import org.readium.r2.shared.util.format.RwpmSniffer +import org.readium.r2.shared.util.format.W3cWpubSniffer +import org.readium.r2.shared.util.format.ZipSniffer import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.borrow @@ -30,9 +49,33 @@ import org.readium.r2.shared.util.use import org.readium.r2.shared.util.zip.ZipArchiveOpener public class AssetSniffer( - private val formatSniffer: FormatSniffer = DefaultFormatSniffer(), + private val formatSniffers: List = defaultFormatSniffers, private val archiveOpener: ArchiveOpener = ZipArchiveOpener() ) { + + public companion object { + + public val defaultFormatSniffers: List = listOf( + ZipSniffer, + RarSniffer, + EpubSniffer, + LpfSniffer, + ArchiveSniffer, + RpfSniffer, + PdfSniffer, + HtmlSniffer, + BitmapSniffer, + AudioSniffer, + JsonSniffer, + OpdsSniffer, + LcpLicenseSniffer, + LcpSniffer, + AdeptSniffer, + W3cWpubSniffer, + RwpmSniffer + ) + } + public suspend fun sniff( file: File, hints: FormatHints = FormatHints() @@ -100,30 +143,42 @@ public class AssetSniffer( private suspend fun doSniff( format: Format?, source: Either>, - hints: FormatHints + hints: FormatHints, + excludeHintsSniffer: FormatHintsSniffer? = null, + excludeBlobSniffer: BlobSniffer? = null, + excludeContainerSniffer: ContainerSniffer? = null ): Try { - formatSniffer - .sniffHints(format, hints) - ?.takeIf { format == null || it.conformsTo(format) } - ?.takeIf { it != format } - ?.let { return doSniff(it, source, hints) } + for (sniffer in formatSniffers) { + sniffer + .takeIf { it != excludeHintsSniffer } + ?.sniffHints(format, hints) + ?.takeIf { format == null || it.conformsTo(format) } + ?.takeIf { it != format } + ?.let { return doSniff(it, source, hints, excludeHintsSniffer = sniffer) } + } when (source) { is Either.Left -> - formatSniffer - .sniffBlob(format, source.value) - .getOrElse { return Try.failure(it) } - ?.takeIf { format == null || it.conformsTo(format) } - ?.takeIf { it != format } - ?.let { return doSniff(it, source, hints) } + for (sniffer in formatSniffers) { + sniffer + .takeIf { it != excludeBlobSniffer } + ?.sniffBlob(format, source.value) + ?.getOrElse { return Try.failure(it) } + ?.takeIf { format == null || it.conformsTo(format) } + ?.takeIf { it != format } + ?.let { return doSniff(it, source, hints, excludeBlobSniffer = sniffer) } + } is Either.Right -> - formatSniffer - .sniffContainer(format, source.value) - .getOrElse { return Try.failure(it) } - ?.takeIf { format == null || it.conformsTo(format) } - ?.takeIf { it != format } - ?.let { return doSniff(it, source, hints) } + for (sniffer in formatSniffers) { + sniffer + .takeIf { it != excludeContainerSniffer } + ?.sniffContainer(format, source.value) + ?.getOrElse { return Try.failure(it) } + ?.takeIf { format == null || it.conformsTo(format) } + ?.takeIf { it != format } + ?.let { return doSniff(it, source, hints, excludeContainerSniffer = sniffer) } + } } if (source is Either.Left) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt index 7f6d56349b..92b0ef2f39 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/android/AndroidDownloadManager.kt @@ -117,7 +117,7 @@ public class AndroidDownloadManager internal constructor( val androidRequest = createRequest( uri = request.url.toUri(), - filename = generateFileName(extension = request.url.extension), + filename = generateFileName(extension = request.url.extension?.value), headers = request.headers ) val downloadId = downloadManager.enqueue(androidRequest) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt index d51175a396..0c8e62c345 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/DefaultSniffers.kt @@ -748,7 +748,7 @@ public object ArchiveSniffer : FormatSniffer { fun archiveContainsOnlyExtensions(fileExtensions: List): Boolean = container.all { url -> - isIgnored(url) || url.extension?.let { + isIgnored(url) || url.extension?.value?.let { fileExtensions.contains( it.lowercase(Locale.ROOT) ) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt index 709981f753..fe71f3c262 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatHints.kt @@ -7,6 +7,7 @@ package org.readium.r2.shared.util.format import java.nio.charset.Charset +import org.readium.r2.shared.util.FileExtension import org.readium.r2.shared.util.mediatype.MediaType /** diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt index b3969b5a6a..ec8db6eb6d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/format/FormatRegistry.kt @@ -6,12 +6,9 @@ package org.readium.r2.shared.util.format +import org.readium.r2.shared.util.FileExtension import org.readium.r2.shared.util.mediatype.MediaType -@JvmInline -public value class FileExtension( - public val value: String -) public data class FormatInfo( public val mediaType: MediaType, public val fileExtension: FileExtension diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/UrlTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/UrlTest.kt index 8b377b249f..d887abc74b 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/UrlTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/UrlTest.kt @@ -148,21 +148,21 @@ class UrlTest { @Test fun getExtension() { - assertEquals("txt", Url("foo/bar.txt?query#fragment")?.extension) + assertEquals("txt", Url("foo/bar.txt?query#fragment")?.extension?.value) assertEquals(null, Url("foo/bar?query#fragment")?.extension) assertEquals(null, Url("foo/bar/?query#fragment")?.extension) - assertEquals("txt", Url("http://example.com/foo/bar.txt?query#fragment")?.extension) + assertEquals("txt", Url("http://example.com/foo/bar.txt?query#fragment")?.extension?.value) assertEquals(null, Url("http://example.com/foo/bar?query#fragment")?.extension) assertEquals(null, Url("http://example.com/foo/bar/")?.extension) - assertEquals("txt", Url("file:///foo/bar.txt?query#fragment")?.extension) + assertEquals("txt", Url("file:///foo/bar.txt?query#fragment")?.extension?.value) assertEquals(null, Url("file:///foo/bar?query#fragment")?.extension) assertEquals(null, Url("file:///foo/bar/")?.extension) } @Test fun extensionIsPercentDecoded() { - assertEquals("%bar", Url("foo.%25bar")?.extension) - assertEquals("%bar", Url("http://example.com/foo.%25bar")?.extension) + assertEquals("%bar", Url("foo.%25bar")?.extension?.value) + assertEquals("%bar", Url("http://example.com/foo.%25bar")?.extension?.value) } @Test diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt index eb0ae2dd9d..c82c6cab71 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/AssetSnifferTest.kt @@ -11,12 +11,12 @@ import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith import org.readium.r2.shared.Fixtures +import org.readium.r2.shared.util.FileExtension import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.data.EmptyContainer -import org.readium.r2.shared.util.format.FileExtension import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatHints import org.readium.r2.shared.util.format.Trait diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt index 540cd71de1..841ad59a0a 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/mediatype/FormatRegistryTest.kt @@ -3,7 +3,7 @@ package org.readium.r2.shared.util.mediatype import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking import org.junit.Test -import org.readium.r2.shared.util.format.FileExtension +import org.readium.r2.shared.util.FileExtension import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatInfo import org.readium.r2.shared.util.format.FormatRegistry diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt index 19726b743a..e1dafb8f81 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt @@ -11,13 +11,13 @@ package org.readium.r2.streamer.extensions import java.io.File import org.readium.r2.shared.extensions.addPrefix +import org.readium.r2.shared.util.FileExtension import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError -import org.readium.r2.shared.util.format.FileExtension import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.SingleResourceContainer @@ -43,7 +43,7 @@ internal fun Iterable.pathCommonFirstComponent(): File? = ?.let { File(it) } internal fun Resource.toContainer( - entryExtension: FileExtension? = sourceUrl?.extension?.let { FileExtension((it)) } + entryExtension: FileExtension? = sourceUrl?.extension ): Container { // Historically, the reading order of a standalone file contained a single link with the // HREF "/$assetName". This was fragile if the asset named changed, or was different on From 365b9aebe56a09e9712e24fbfe375e133aea60a2 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Wed, 20 Dec 2023 19:42:31 +0100 Subject: [PATCH 84/86] Various changes --- .../protection/ContentProtection.kt | 2 +- .../protection/FallbackContentProtection.kt | 48 ++++++++++++-- .../FallbackContentProtectionService.kt | 41 ------------ .../r2/shared/util/asset/ArchiveOpener.kt | 14 ++-- .../r2/shared/util/asset/AssetOpener.kt | 4 +- .../r2/shared/util/asset/AssetSniffer.kt | 1 - .../shared/util/zip/FileZipArchiveProvider.kt | 6 +- .../util/zip/StreamingZipArchiveProvider.kt | 17 +++-- .../r2/shared/util/zip/ZipArchiveOpener.kt | 3 +- .../shared/util/resource/ZipContainerTest.kt | 1 + ...icationFactory.kt => PublicationOpener.kt} | 8 +-- .../r2/streamer/extensions/Container.kt | 20 ++++-- .../r2/streamer/parser/audio/AudioParser.kt | 64 +++++++++++++----- .../r2/streamer/parser/epub/EpubParser.kt | 4 +- .../r2/streamer/parser/image/ImageParser.kt | 66 +++++++++++++------ .../r2/streamer/parser/pdf/PdfParser.kt | 7 +- .../streamer/parser/image/ImageParserTest.kt | 9 ++- .../org/readium/r2/testapp/Application.kt | 2 +- .../java/org/readium/r2/testapp/Readium.kt | 4 +- .../readium/r2/testapp/domain/Bookshelf.kt | 6 +- .../r2/testapp/domain/PublicationError.kt | 8 +-- .../r2/testapp/reader/ReaderRepository.kt | 2 +- 22 files changed, 201 insertions(+), 136 deletions(-) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtectionService.kt rename readium/streamer/src/main/java/org/readium/r2/streamer/{PublicationFactory.kt => PublicationOpener.kt} (96%) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt index e536aa7c81..d75f9bd937 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/ContentProtection.kt @@ -42,7 +42,7 @@ public interface ContentProtection { } /** - * Holds the result of opening an [OpenResult] with a [ContentProtection]. + * Holds the result of opening an [Asset] with a [ContentProtection]. * * @property asset Asset pointing to a publication. * @property onCreatePublication Called on every parsed Publication.Builder diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt index af3e239bf0..e814003665 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtection.kt @@ -6,19 +6,20 @@ package org.readium.r2.shared.publication.protection -import org.readium.r2.shared.InternalReadiumApi +import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.protection.ContentProtection.Scheme +import org.readium.r2.shared.publication.services.ContentProtectionService import org.readium.r2.shared.publication.services.contentProtectionServiceFactory +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.format.Trait /** - * [ContentProtection] implementation used as a fallback by the PublicationFactory to detect DRMs + * [ContentProtection] implementation used as a fallback by when detecting known DRMs * not supported by the app. */ -@InternalReadiumApi public class FallbackContentProtection : ContentProtection { override suspend fun open( @@ -34,9 +35,9 @@ public class FallbackContentProtection : ContentProtection { val protectionServiceFactory = when { asset.format.conformsTo(Trait.LCP_PROTECTED) -> - FallbackContentProtectionService.createFactory(Scheme.Lcp, "Readium LCP") + Service.createFactory(Scheme.Lcp, "Readium LCP") asset.format.conformsTo(Trait.ADEPT_PROTECTED) -> - FallbackContentProtectionService.createFactory(Scheme.Adept, "Adobe ADEPT") + Service.createFactory(Scheme.Adept, "Adobe ADEPT") else -> return Try.failure(ContentProtection.OpenError.AssetNotSupported()) } @@ -50,4 +51,41 @@ public class FallbackContentProtection : ContentProtection { return Try.success(protectedFile) } + + public class SchemeNotSupportedError( + public val scheme: Scheme, + public val name: String + ) : Error { + + override val message: String = "$name DRM scheme is not supported." + + override val cause: Error? = null + } + + private class Service( + override val scheme: Scheme, + override val name: String + ) : ContentProtectionService { + + override val isRestricted: Boolean = + true + + override val credentials: String? = + null + + override val rights: ContentProtectionService.UserRights = + ContentProtectionService.UserRights.AllRestricted + + override val error: Error = + SchemeNotSupportedError(scheme, name) + + companion object { + + fun createFactory( + scheme: Scheme, + name: String + ): (Publication.Service.Context) -> ContentProtectionService = + { Service(scheme, name) } + } + } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtectionService.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtectionService.kt deleted file mode 100644 index 1885eda641..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/publication/protection/FallbackContentProtectionService.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2023 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.shared.publication.protection - -import org.readium.r2.shared.publication.Publication -import org.readium.r2.shared.publication.services.ContentProtectionService -import org.readium.r2.shared.util.Error - -internal class FallbackContentProtectionService( - override val scheme: ContentProtection.Scheme, - override val name: String -) : ContentProtectionService { - - override val isRestricted: Boolean = true - override val credentials: String? = null - override val rights = ContentProtectionService.UserRights.AllRestricted - override val error: Error = SchemeNotSupportedError(scheme, name) - - private class SchemeNotSupportedError( - val scheme: ContentProtection.Scheme, - val name: String - ) : Error { - - override val message: String = "$name DRM scheme is not supported." - - override val cause: Error? = null - } - - companion object { - - fun createFactory( - scheme: ContentProtection.Scheme, - name: String - ): (Publication.Service.Context) -> ContentProtectionService = - { FallbackContentProtectionService(scheme, name) } - } -} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ArchiveOpener.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ArchiveOpener.kt index 273f4d445e..5dd94049a3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ArchiveOpener.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/ArchiveOpener.kt @@ -34,15 +34,15 @@ public interface ArchiveOpener { } /** - * Creates a new [Container] to access the entries of the given archive. + * Creates a new [Container] to access the entries of an archive with a known format. */ public suspend fun open( format: Format, source: Readable - ): Try, OpenError> + ): Try /** - * Creates a new [Container] to access the entries of the given archive. + * Creates a new [ContainerAsset] to access the entries of an archive after sniffing its format. */ public suspend fun sniffOpen( source: Readable @@ -54,7 +54,7 @@ public interface ArchiveOpener { * the format. */ public class CompositeArchiveOpener( - private val factories: List + private val openers: List ) : ArchiveOpener { public constructor(vararg factories: ArchiveOpener) : @@ -63,8 +63,8 @@ public class CompositeArchiveOpener( override suspend fun open( format: Format, source: Readable - ): Try, ArchiveOpener.OpenError> { - for (factory in factories) { + ): Try { + for (factory in openers) { factory.open(format, source) .getOrElse { error -> when (error) { @@ -79,7 +79,7 @@ public class CompositeArchiveOpener( } override suspend fun sniffOpen(source: Readable): Try { - for (factory in factories) { + for (factory in openers) { factory.sniffOpen(source) .getOrElse { error -> when (error) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt index 7885d3d79a..0f25878dc3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt @@ -68,7 +68,7 @@ public class AssetOpener( val resource = retrieveResource(url) .getOrElse { return Try.failure(it) } - val archive = archiveOpener + val asset = archiveOpener .open(format, resource) .getOrElse { return when (it) { @@ -79,7 +79,7 @@ public class AssetOpener( } } - return Try.success(ContainerAsset(format, archive)) + return Try.success(asset) } private suspend fun retrieveResource( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt index 18f7e31f3a..e3bf37f938 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetSniffer.kt @@ -206,7 +206,6 @@ public class AssetSniffer( } } else { archiveOpener.open(format, source) - .map { ContainerAsset(format, it) } .tryRecover { when (it) { is ArchiveOpener.OpenError.FormatNotSupported -> diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index 96bff8f64e..db9f68bc19 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt @@ -56,7 +56,7 @@ internal class FileZipArchiveProvider { suspend fun open( format: Format, file: File - ): Try, ArchiveOpener.OpenError> { + ): Try { if (!format.conformsTo(Trait.ZIP)) { return Try.failure( ArchiveOpener.OpenError.FormatNotSupported(format) @@ -66,7 +66,9 @@ internal class FileZipArchiveProvider { val container = open(file) .getOrElse { return Try.failure(it) } - return Try.success(container) + val asset = ContainerAsset(format, container) + + return Try.success(asset) } // Internal for testing purpose diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index a63ec3cc0c..5a453aa0ff 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -47,25 +47,28 @@ internal class StreamingZipArchiveProvider { suspend fun open( format: Format, source: Readable - ): Try, ArchiveOpener.OpenError> { + ): Try { if (!format.conformsTo(Trait.ZIP)) { return Try.failure( ArchiveOpener.OpenError.FormatNotSupported(format) ) } - return try { - val container = openBlob( + val container = try { + openBlob( source, ::ReadException, (source as? Resource)?.sourceUrl ) - Try.success(container) } catch (exception: Exception) { - exception.findInstance(ReadException::class.java) - ?.let { Try.failure(ArchiveOpener.OpenError.Reading(it.error)) } - ?: Try.failure(ArchiveOpener.OpenError.Reading(ReadError.Decoding(exception))) + val error = exception.findInstance(ReadException::class.java) + ?.let { ArchiveOpener.OpenError.Reading(it.error) } + ?: ArchiveOpener.OpenError.Reading(ReadError.Decoding(exception)) + + return Try.failure(error) } + + return Try.success(ContainerAsset(format, container)) } private suspend fun openBlob( diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt index ca5f53272d..7d619a8adc 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/ZipArchiveOpener.kt @@ -10,7 +10,6 @@ import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.ArchiveOpener import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.SniffError -import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.resource.Resource @@ -24,7 +23,7 @@ public class ZipArchiveOpener : ArchiveOpener { override suspend fun open( format: Format, source: Readable - ): Try, ArchiveOpener.OpenError> = + ): Try = (source as? Resource)?.sourceUrl?.toFile() ?.let { fileZipArchiveProvider.open(format, it) } ?: streamingZipArchiveProvider.open(format, source) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt index 8074fce11a..31a7367d15 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/resource/ZipContainerTest.kt @@ -47,6 +47,7 @@ class ZipContainerTest(val sut: suspend () -> Container) { file = File(epubZip.path) ) .getOrNull() + ?.container ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationOpener.kt similarity index 96% rename from readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt rename to readium/streamer/src/main/java/org/readium/r2/streamer/PublicationOpener.kt index 684f3e0543..7bf08acb40 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationFactory.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/PublicationOpener.kt @@ -32,7 +32,7 @@ import org.readium.r2.streamer.parser.readium.ReadiumWebPubParser /** * Opens a Publication using a list of parsers. * - * The [PublicationFactory] is configured to use Readium's default parsers, which you can bypass + * The [PublicationOpener] is configured to use Readium's default parsers, which you can bypass * using ignoreDefaultParsers. However, you can provide additional [parsers] which will take * precedence over the default ones. This can also be used to provide an alternative configuration * of a default parser. @@ -47,7 +47,7 @@ import org.readium.r2.streamer.parser.readium.ReadiumWebPubParser * the manifest, the root container or the list of service factories of a [Publication]. */ @OptIn(PdfSupport::class) -public class PublicationFactory( +public class PublicationOpener( context: Context, parsers: List = emptyList(), ignoreDefaultParsers: Boolean = false, @@ -78,7 +78,7 @@ public class PublicationFactory( private val defaultParsers: List = listOfNotNull( EpubParser(), - pdfFactory?.let { PdfParser(context, it) }, + pdfFactory?.let { PdfParser(context, it, formatRegistry) }, ReadiumWebPubParser(context, httpClient, pdfFactory), ImageParser(assetSniffer, formatRegistry), AudioParser(assetSniffer, formatRegistry) @@ -118,7 +118,7 @@ public class PublicationFactory( warnings: WarningLogger? = null ): Try { var compositeOnCreatePublication: Publication.Builder.() -> Unit = { - this@PublicationFactory.onCreatePublication(this) + this@PublicationOpener.onCreatePublication(this) onCreatePublication(this) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt index e1dafb8f81..586efe3608 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt @@ -11,14 +11,15 @@ package org.readium.r2.streamer.extensions import java.io.File import org.readium.r2.shared.extensions.addPrefix -import org.readium.r2.shared.util.FileExtension import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.AssetSniffer +import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.SingleResourceContainer import org.readium.r2.shared.util.use @@ -42,20 +43,27 @@ internal fun Iterable.pathCommonFirstComponent(): File? = ?.firstOrNull() ?.let { File(it) } -internal fun Resource.toContainer( - entryExtension: FileExtension? = sourceUrl?.extension +internal fun ResourceAsset.toContainer( + formatRegistry: FormatRegistry ): Container { // Historically, the reading order of a standalone file contained a single link with the // HREF "/$assetName". This was fragile if the asset named changed, or was different on // other devices. To avoid this, we now use a single link with the HREF // "publication.extension". - val extension = entryExtension?.value + val extension = formatRegistry[format] + ?.fileExtension + ?.value + ?: resource.sourceUrl + ?.extension + ?.value + + val dottedExtension = extension ?.addPrefix(".") ?: "" return SingleResourceContainer( - Url("publication$extension")!!, - this + Url("publication$dottedExtension")!!, + resource ) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt index da50d524a6..510779cfc8 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/audio/AudioParser.kt @@ -18,12 +18,14 @@ import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.ResourceAsset +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.format.Trait import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs import org.readium.r2.streamer.extensions.sniffContainerEntries @@ -44,31 +46,44 @@ public class AudioParser( override suspend fun parse( asset: Asset, warnings: WarningLogger? + ): Try = + when (asset) { + is ResourceAsset -> parseResourceAsset(asset) + is ContainerAsset -> parseContainerAsset(asset) + } + + private fun parseResourceAsset( + asset: ResourceAsset ): Try { - if (!asset.format.conformsTo(Trait.AUDIOBOOK) && !asset.format.conformsTo(Trait.AUDIO)) { + if (!asset.format.conformsTo(Trait.AUDIO)) { return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } - val container = when (asset) { - is ResourceAsset -> - asset.resource.toContainer() - is ContainerAsset -> - asset.container + val container = + asset.toContainer(formatRegistry) + + val readingOrderWithFormat = + listOfNotNull(container.first() to asset.format) + + return finalizeParsing(container, readingOrderWithFormat, null) + } + + private suspend fun parseContainerAsset( + asset: ContainerAsset + ): Try { + if (!asset.format.conformsTo(Trait.AUDIOBOOK)) { + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } val entryFormats: Map = assetSniffer - .sniffContainerEntries(container) { !it.isHiddenOrThumbs } + .sniffContainerEntries(asset.container) { !it.isHiddenOrThumbs } .getOrElse { return Try.failure(PublicationParser.ParseError.Reading(it)) } val readingOrderWithFormat = - if (asset.format.conformsTo(Trait.AUDIOBOOK)) { - container - .mapNotNull { url -> entryFormats[url]?.let { url to it } } - .filter { it.second.conformsTo(Trait.AUDIO) } - .sortedBy { it.first.toString() } - } else { - listOfNotNull(container.first() to asset.format) - } + asset.container + .mapNotNull { url -> entryFormats[url]?.let { url to it } } + .filter { it.second.conformsTo(Trait.AUDIO) } + .sortedBy { it.first.toString() } if (readingOrderWithFormat.isEmpty()) { return Try.failure( @@ -80,7 +95,20 @@ public class AudioParser( ) } - val readingOrderLinks = readingOrderWithFormat.map { (url, format) -> + val title = asset + .container + .entries + .guessTitle() + + return finalizeParsing(asset.container, readingOrderWithFormat, title) + } + + private fun finalizeParsing( + container: Container, + readingOrderWithFormat: List>, + title: String? + ): Try { + val readingOrder = readingOrderWithFormat.map { (url, format) -> val mediaType = formatRegistry[format]?.mediaType Link(href = url, mediaType = mediaType) } @@ -88,9 +116,9 @@ public class AudioParser( val manifest = Manifest( metadata = Metadata( conformsTo = setOf(Publication.Profile.AUDIOBOOK), - localizedTitle = container.entries.guessTitle()?.let { LocalizedString(it) } + localizedTitle = title?.let { LocalizedString(it) } ), - readingOrder = readingOrderLinks + readingOrder = readingOrder ) val publicationBuilder = Publication.Builder( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 4ada5b3daa..73c4e77dfd 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -25,7 +25,7 @@ import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.data.decodeXml import org.readium.r2.shared.util.data.readDecodeOrElse import org.readium.r2.shared.util.data.readDecodeOrNull -import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.Trait import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType @@ -51,7 +51,7 @@ public class EpubParser( asset: Asset, warnings: WarningLogger? ): Try { - if (asset !is ContainerAsset || !asset.format.conformsTo(Format.EPUB)) { + if (asset !is ContainerAsset || !asset.format.conformsTo(Trait.EPUB)) { return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt index 01f1074714..744e5af764 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/image/ImageParser.kt @@ -19,6 +19,7 @@ import org.readium.r2.shared.util.asset.Asset import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.ResourceAsset +import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatRegistry @@ -26,6 +27,7 @@ import org.readium.r2.shared.util.format.Trait import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.mediatype.MediaType +import org.readium.r2.shared.util.resource.Resource import org.readium.r2.streamer.extensions.guessTitle import org.readium.r2.streamer.extensions.isHiddenOrThumbs import org.readium.r2.streamer.extensions.sniffContainerEntries @@ -46,31 +48,44 @@ public class ImageParser( override suspend fun parse( asset: Asset, warnings: WarningLogger? + ): Try = + when (asset) { + is ResourceAsset -> parseResourceAsset(asset) + is ContainerAsset -> parseContainerAsset(asset) + } + + private fun parseResourceAsset( + asset: ResourceAsset ): Try { - if (!asset.format.conformsTo(Trait.COMICS) && !asset.format.conformsTo(Trait.BITMAP)) { + if (!asset.format.conformsTo(Trait.BITMAP)) { return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } - val container = when (asset) { - is ResourceAsset -> - asset.resource.toContainer() - is ContainerAsset -> - asset.container + val container = + asset.toContainer(formatRegistry) + + val readingOrderWithFormat = + listOfNotNull(container.first() to asset.format) + + return finalizeParsing(container, readingOrderWithFormat, null) + } + + private suspend fun parseContainerAsset( + asset: ContainerAsset + ): Try { + if (!asset.format.conformsTo(Trait.COMICS)) { + return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } val entryFormats: Map = assetSniffer - .sniffContainerEntries(container) { !it.isHiddenOrThumbs } + .sniffContainerEntries(asset.container) { !it.isHiddenOrThumbs } .getOrElse { return Try.failure(PublicationParser.ParseError.Reading(it)) } val readingOrderWithFormat = - if (asset.format.conformsTo(Trait.COMICS)) { - container - .mapNotNull { url -> entryFormats[url]?.let { url to it } } - .filter { it.second.conformsTo(Trait.BITMAP) } - .sortedBy { it.first.toString() } - } else { - listOfNotNull(container.first() to asset.format) - } + asset.container + .mapNotNull { url -> entryFormats[url]?.let { url to it } } + .filter { it.second.conformsTo(Trait.BITMAP) } + .sortedBy { it.first.toString() } if (readingOrderWithFormat.isEmpty()) { return Try.failure( @@ -82,20 +97,33 @@ public class ImageParser( ) } - val readingOrderLinks = readingOrderWithFormat.map { (url, format) -> + val title = asset + .container + .entries + .guessTitle() + + return finalizeParsing(asset.container, readingOrderWithFormat, title) + } + + private fun finalizeParsing( + container: Container, + readingOrderWithFormat: List>, + title: String? + ): Try { + val readingOrder = readingOrderWithFormat.map { (url, format) -> val mediaType = formatRegistry[format]?.mediaType Link(href = url, mediaType = mediaType) }.toMutableList() // First valid resource is the cover. - readingOrderLinks[0] = readingOrderLinks[0].copy(rels = setOf("cover")) + readingOrder[0] = readingOrder[0].copy(rels = setOf("cover")) val manifest = Manifest( metadata = Metadata( conformsTo = setOf(Publication.Profile.DIVINA), - localizedTitle = container.guessTitle()?.let { LocalizedString(it) } + localizedTitle = title?.let { LocalizedString(it) } ), - readingOrder = readingOrderLinks + readingOrder = readingOrder ) val publicationBuilder = Publication.Builder( diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt index a7dea73d6d..cd61610351 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/pdf/PdfParser.kt @@ -32,7 +32,8 @@ import org.readium.r2.streamer.parser.PublicationParser @OptIn(ExperimentalReadiumApi::class) public class PdfParser( context: Context, - private val pdfFactory: PdfDocumentFactory<*> + private val pdfFactory: PdfDocumentFactory<*>, + private val formatRegistry: FormatRegistry ) : PublicationParser { private val context = context.applicationContext @@ -45,8 +46,8 @@ public class PdfParser( return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } - val container = asset.resource - .toContainer(FormatRegistry()[Format.PDF]?.fileExtension) + val container = asset + .toContainer(formatRegistry) val url = container.entries .first() diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt index f60dd07b5a..848f000573 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt +++ b/readium/streamer/src/test/java/org/readium/r2/streamer/parser/image/ImageParserTest.kt @@ -21,13 +21,12 @@ import org.readium.r2.shared.publication.firstWithRel import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.asset.ContainerAsset +import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.mediatype.MediaType -import org.readium.r2.shared.util.resource.SingleResourceContainer -import org.readium.r2.shared.util.toUrl import org.readium.r2.shared.util.zip.ZipArchiveOpener import org.readium.r2.streamer.parseBlocking import org.robolectric.RobolectricTestRunner @@ -47,15 +46,15 @@ class ImageParserTest { val file = fileForResource("futuristic_tales.cbz") val resource = FileResource(file) val archive = archiveOpener.open(Format.ZIP, resource).checkSuccess() - ContainerAsset(Format.CBZ, archive) + ContainerAsset(Format.CBZ, archive.container) } private val jpgAsset = runBlocking { val file = fileForResource("futuristic_tales.jpg") val resource = FileResource(file, mediaType = MediaType.JPEG) - ContainerAsset( + ResourceAsset( Format.JPEG, - SingleResourceContainer(file.toUrl(), resource) + resource ) } diff --git a/test-app/src/main/java/org/readium/r2/testapp/Application.kt b/test-app/src/main/java/org/readium/r2/testapp/Application.kt index f3cf6be403..5f744a9651 100755 --- a/test-app/src/main/java/org/readium/r2/testapp/Application.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Application.kt @@ -78,7 +78,7 @@ class Application : android.app.Application() { Bookshelf( bookRepository, CoverStorage(storageDir, httpClient = readium.httpClient), - readium.publicationFactory, + readium.publicationOpener, readium.assetOpener, readium.formatRegistry, createPublicationRetriever = { listener -> diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 97d0a0127f..1ffd23cc45 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -26,7 +26,7 @@ import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.http.HttpResourceFactory import org.readium.r2.shared.util.resource.CompositeResourceFactory import org.readium.r2.shared.util.zip.ZipArchiveOpener -import org.readium.r2.streamer.PublicationFactory +import org.readium.r2.streamer.PublicationOpener /** * Holds the shared Readium objects and services used by the app. @@ -82,7 +82,7 @@ class Readium(context: Context) { /** * The PublicationFactory is used to parse and open publications. */ - val publicationFactory = PublicationFactory( + val publicationOpener = PublicationOpener( context, contentProtections = contentProtections, formatRegistry = formatRegistry, diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index 031c60202e..3b95211a4f 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -22,7 +22,7 @@ import org.readium.r2.shared.util.format.FormatRegistry import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.toUrl -import org.readium.r2.streamer.PublicationFactory +import org.readium.r2.streamer.PublicationOpener import org.readium.r2.testapp.data.BookRepository import org.readium.r2.testapp.data.model.Book import org.readium.r2.testapp.utils.extensions.formatPercentage @@ -38,7 +38,7 @@ import timber.log.Timber class Bookshelf( private val bookRepository: BookRepository, private val coverStorage: CoverStorage, - private val publicationFactory: PublicationFactory, + private val publicationOpener: PublicationOpener, private val assetOpener: AssetOpener, private val formatRegistry: FormatRegistry, createPublicationRetriever: (PublicationRetriever.Listener) -> PublicationRetriever @@ -132,7 +132,7 @@ class Bookshelf( ) } - publicationFactory.open( + publicationOpener.open( asset, allowUserInteraction = false ).onSuccess { publication -> diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt index 1003b87039..6e49f98f0a 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/PublicationError.kt @@ -8,7 +8,7 @@ package org.readium.r2.testapp.domain import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.asset.AssetOpener -import org.readium.r2.streamer.PublicationFactory +import org.readium.r2.streamer.PublicationOpener import org.readium.r2.testapp.R import org.readium.r2.testapp.utils.UserError @@ -58,11 +58,11 @@ sealed class PublicationError( UnsupportedScheme(error) } - operator fun invoke(error: PublicationFactory.OpenError): PublicationError = + operator fun invoke(error: PublicationOpener.OpenError): PublicationError = when (error) { - is PublicationFactory.OpenError.Reading -> + is PublicationOpener.OpenError.Reading -> ReadError(error.cause) - is PublicationFactory.OpenError.FormatNotSupported -> + is PublicationOpener.OpenError.FormatNotSupported -> PublicationError(error) } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt index 9b0ae42868..c8e8880461 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/reader/ReaderRepository.kt @@ -83,7 +83,7 @@ class ReaderRepository( ) } - val publication = readium.publicationFactory.open( + val publication = readium.publicationOpener.open( asset, allowUserInteraction = true ).getOrElse { From 76a9543bbb266f77907eeb157e4eaf713b8693b7 Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Thu, 21 Dec 2023 09:59:14 +0100 Subject: [PATCH 85/86] Various changes --- .../readium/r2/lcp/LcpContentProtection.kt | 1 + .../publication/epub}/EpubEncryptionParser.kt | 10 +-- .../publication}/epub/EncryptionParserTest.kt | 4 +- .../encryption/encryption-lcp-prefixes.xml | 0 .../epub/encryption/encryption-lcp-xmlns.xml | 0 .../encryption/encryption-unknown-method.xml | 0 .../streamer/parser/epub/EncryptionParser.kt | 69 ------------------- .../r2/streamer/parser/epub/EpubParser.kt | 3 +- .../readium/r2/testapp/data/BookRepository.kt | 3 - .../org/readium/r2/testapp/data/model/Book.kt | 9 --- .../readium/r2/testapp/domain/Bookshelf.kt | 1 - 11 files changed, 11 insertions(+), 89 deletions(-) rename readium/{lcp/src/main/java/org/readium/r2/lcp => shared/src/main/java/org/readium/r2/shared/publication/epub}/EpubEncryptionParser.kt (92%) rename readium/{streamer/src/test/java/org/readium/r2/streamer/parser => shared/src/test/java/org/readium/r2/shared/publication}/epub/EncryptionParserTest.kt (96%) rename readium/{streamer/src/test/resources/org/readium/r2/streamer/parser => shared/src/test/resources/org/readium/r2/shared/publication}/epub/encryption/encryption-lcp-prefixes.xml (100%) rename readium/{streamer/src/test/resources/org/readium/r2/streamer/parser => shared/src/test/resources/org/readium/r2/shared/publication}/epub/encryption/encryption-lcp-xmlns.xml (100%) rename readium/{streamer/src/test/resources/org/readium/r2/streamer/parser => shared/src/test/resources/org/readium/r2/shared/publication}/epub/encryption/encryption-unknown-method.xml (100%) delete mode 100644 readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EncryptionParser.kt diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt index de9e8cf389..f5843e389e 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt @@ -10,6 +10,7 @@ import org.readium.r2.lcp.auth.LcpPassphraseAuthentication import org.readium.r2.lcp.license.model.LicenseDocument import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.publication.encryption.encryption +import org.readium.r2.shared.publication.epub.EpubEncryptionParser import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.publication.services.contentProtectionServiceFactory import org.readium.r2.shared.util.AbsoluteUrl diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/EpubEncryptionParser.kt b/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/EpubEncryptionParser.kt similarity index 92% rename from readium/lcp/src/main/java/org/readium/r2/lcp/EpubEncryptionParser.kt rename to readium/shared/src/main/java/org/readium/r2/shared/publication/epub/EpubEncryptionParser.kt index 5446f8a0e2..5e208554ce 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/EpubEncryptionParser.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/publication/epub/EpubEncryptionParser.kt @@ -4,22 +4,24 @@ * available in the top-level LICENSE file of the project. */ -package org.readium.r2.lcp +package org.readium.r2.shared.publication.epub +import org.readium.r2.shared.InternalReadiumApi import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.publication.protection.ContentProtection import org.readium.r2.shared.util.Url import org.readium.r2.shared.util.xml.ElementNode -internal object EpubEncryptionParser { +@InternalReadiumApi +public object EpubEncryptionParser { - object Namespaces { + private object Namespaces { const val ENC = "http://www.w3.org/2001/04/xmlenc#" const val SIG = "http://www.w3.org/2000/09/xmldsig#" const val COMP = "http://www.idpf.org/2016/encryption#compression" } - fun parse(document: ElementNode): Map = + public fun parse(document: ElementNode): Map = document.get("EncryptedData", Namespaces.ENC) .mapNotNull { parseEncryptedData(it) } .toMap() diff --git a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EncryptionParserTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/publication/epub/EncryptionParserTest.kt similarity index 96% rename from readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EncryptionParserTest.kt rename to readium/shared/src/test/java/org/readium/r2/shared/publication/epub/EncryptionParserTest.kt index 927a477374..cae1bba7d5 100644 --- a/readium/streamer/src/test/java/org/readium/r2/streamer/parser/epub/EncryptionParserTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/publication/epub/EncryptionParserTest.kt @@ -7,7 +7,7 @@ * LICENSE file present in the project repository where this source code is maintained. */ -package org.readium.r2.streamer.parser.epub +package org.readium.r2.shared.publication.epub import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.entry @@ -24,7 +24,7 @@ class EncryptionParserTest { val res = EncryptionParserTest::class.java.getResourceAsStream(path) checkNotNull(res) val document = XmlParser().parse(res) - return EncryptionParser.parse(document) + return EpubEncryptionParser.parse(document) } val lcpChap1 = entry( diff --git a/readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/encryption/encryption-lcp-prefixes.xml b/readium/shared/src/test/resources/org/readium/r2/shared/publication/epub/encryption/encryption-lcp-prefixes.xml similarity index 100% rename from readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/encryption/encryption-lcp-prefixes.xml rename to readium/shared/src/test/resources/org/readium/r2/shared/publication/epub/encryption/encryption-lcp-prefixes.xml diff --git a/readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/encryption/encryption-lcp-xmlns.xml b/readium/shared/src/test/resources/org/readium/r2/shared/publication/epub/encryption/encryption-lcp-xmlns.xml similarity index 100% rename from readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/encryption/encryption-lcp-xmlns.xml rename to readium/shared/src/test/resources/org/readium/r2/shared/publication/epub/encryption/encryption-lcp-xmlns.xml diff --git a/readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/encryption/encryption-unknown-method.xml b/readium/shared/src/test/resources/org/readium/r2/shared/publication/epub/encryption/encryption-unknown-method.xml similarity index 100% rename from readium/streamer/src/test/resources/org/readium/r2/streamer/parser/epub/encryption/encryption-unknown-method.xml rename to readium/shared/src/test/resources/org/readium/r2/shared/publication/epub/encryption/encryption-unknown-method.xml diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EncryptionParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EncryptionParser.kt deleted file mode 100644 index 0ac575dcfe..0000000000 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EncryptionParser.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2022 Readium Foundation. All rights reserved. - * Use of this source code is governed by the BSD-style license - * available in the top-level LICENSE file of the project. - */ - -package org.readium.r2.streamer.parser.epub - -import org.readium.r2.shared.publication.encryption.Encryption -import org.readium.r2.shared.publication.protection.ContentProtection -import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.xml.ElementNode -import org.readium.r2.streamer.parser.epub.extensions.fromEpubHref - -internal object EncryptionParser { - fun parse(document: ElementNode): Map = - document.get("EncryptedData", Namespaces.ENC) - .mapNotNull { parseEncryptedData(it) } - .toMap() - - private fun parseEncryptedData(node: ElementNode): Pair? { - val resourceURI = node.getFirst("CipherData", Namespaces.ENC) - ?.getFirst("CipherReference", Namespaces.ENC)?.getAttr("URI") - ?.let { Url.fromEpubHref(it) } - ?: return null - val retrievalMethod = node.getFirst("KeyInfo", Namespaces.SIG) - ?.getFirst("RetrievalMethod", Namespaces.SIG)?.getAttr("URI") - val scheme = if (retrievalMethod == "license.lcpl#/encryption/content_key") { - ContentProtection.Scheme.Lcp.uri - } else { - null - } - val algorithm = node.getFirst("EncryptionMethod", Namespaces.ENC) - ?.getAttr("Algorithm") - ?: return null - val compression = node.getFirst("EncryptionProperties", Namespaces.ENC) - ?.let { parseEncryptionProperties(it) } - val originalLength = compression?.first - val compressionMethod = compression?.second - val enc = Encryption( - scheme = scheme, - /* profile = drm?.license?.encryptionProfile, - FIXME: This has probably never worked. Profile needs to be filled somewhere, though. */ - algorithm = algorithm, - compression = compressionMethod, - originalLength = originalLength - ) - return Pair(resourceURI, enc) - } - - private fun parseEncryptionProperties(encryptionProperties: ElementNode): Pair? { - for (encryptionProperty in encryptionProperties.get("EncryptionProperty", Namespaces.ENC)) { - val compressionElement = encryptionProperty.getFirst("Compression", Namespaces.COMP) - if (compressionElement != null) { - parseCompressionElement(compressionElement)?.let { return it } - } - } - return null - } - - private fun parseCompressionElement(compressionElement: ElementNode): Pair? { - val originalLength = compressionElement.getAttr("OriginalLength")?.toLongOrNull() - ?: return null - val method = compressionElement.getAttr("Method") - ?: return null - val compression = if (method == "8") "deflate" else "none" - return Pair(originalLength, compression) - } -} diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt index 73c4e77dfd..7a51868d69 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/EpubParser.kt @@ -10,6 +10,7 @@ import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.publication.Link import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.encryption.Encryption +import org.readium.r2.shared.publication.epub.EpubEncryptionParser import org.readium.r2.shared.publication.services.content.DefaultContentService import org.readium.r2.shared.publication.services.content.iterators.HtmlResourceContentIterator import org.readium.r2.shared.publication.services.search.StringSearchService @@ -141,7 +142,7 @@ public class EpubParser( private suspend fun parseEncryptionData(container: Container): Map = container.readDecodeXmlOrNull(path = "META-INF/encryption.xml") - ?.let { EncryptionParser.parse(it) } + ?.let { EpubEncryptionParser.parse(it) } ?: emptyMap() private suspend fun parseNavigationData( diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt b/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt index e7e85c0276..14d0bed426 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt @@ -15,7 +15,6 @@ import org.readium.r2.shared.publication.Locator import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.indexOfFirstWithHref import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.testapp.data.db.BooksDao import org.readium.r2.testapp.data.model.Book @@ -81,7 +80,6 @@ class BookRepository( suspend fun insertBook( url: Url, - format: Format, mediaType: MediaType, publication: Publication, cover: File @@ -92,7 +90,6 @@ class BookRepository( author = publication.metadata.authorName, href = url.toString(), identifier = publication.metadata.identifier ?: "", - format = format, mediaType = mediaType, progression = "{}", cover = cover.path diff --git a/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt b/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt index 13ee1b0016..2f9d32eaa4 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/data/model/Book.kt @@ -10,7 +10,6 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import org.readium.r2.shared.util.AbsoluteUrl -import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.mediatype.MediaType @Entity(tableName = Book.TABLE_NAME) @@ -32,8 +31,6 @@ data class Book( val progression: String? = null, @ColumnInfo(name = MEDIA_TYPE) val rawMediaType: String, - @ColumnInfo(name = FORMAT_ID) - val formatId: String, @ColumnInfo(name = COVER) val cover: String ) { @@ -46,7 +43,6 @@ data class Book( author: String? = null, identifier: String, progression: String? = null, - format: Format, mediaType: MediaType, cover: String ) : this( @@ -57,7 +53,6 @@ data class Book( author = author, identifier = identifier, progression = progression, - formatId = format.toString(), rawMediaType = mediaType.toString(), cover = cover ) @@ -67,9 +62,6 @@ data class Book( val mediaType: MediaType get() = MediaType(rawMediaType)!! - val format: Format get() = - Format(formatId) - companion object { const val TABLE_NAME = "books" @@ -82,6 +74,5 @@ data class Book( const val PROGRESSION = "progression" const val MEDIA_TYPE = "media_type" const val COVER = "cover" - const val FORMAT_ID = "format_id" } } diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index 3b95211a4f..827348270b 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -148,7 +148,6 @@ class Bookshelf( val id = bookRepository.insertBook( url, - asset.format, formatRegistry[asset.format]?.mediaType ?: MediaType.BINARY, publication, coverFile From 07a6f25df2f659b83f762b80def7512b77845b6b Mon Sep 17 00:00:00 2001 From: Quentin Gliosca Date: Thu, 21 Dec 2023 15:32:02 +0100 Subject: [PATCH 86/86] Changes --- .../src/main/java/org/readium/r2/shared/util/Url.kt | 6 ++++++ .../org/readium/r2/streamer/extensions/Container.kt | 10 ++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt index 9c01bc16a0..85e9744ce3 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/Url.kt @@ -360,3 +360,9 @@ private fun String.isValidUrl(): Boolean = public value class FileExtension( public val value: String ) + +/** + * Appends this file extension to [filename]. + */ +public fun FileExtension?.appendToFilename(filename: String): String = + this?.let { "$filename.$value" } ?: filename diff --git a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt index 586efe3608..66e8a05e4e 100644 --- a/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt +++ b/readium/streamer/src/main/java/org/readium/r2/streamer/extensions/Container.kt @@ -10,9 +10,9 @@ package org.readium.r2.streamer.extensions import java.io.File -import org.readium.r2.shared.extensions.addPrefix import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.Url +import org.readium.r2.shared.util.appendToFilename import org.readium.r2.shared.util.asset.AssetSniffer import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.asset.SniffError @@ -52,17 +52,11 @@ internal fun ResourceAsset.toContainer( // "publication.extension". val extension = formatRegistry[format] ?.fileExtension - ?.value ?: resource.sourceUrl ?.extension - ?.value - - val dottedExtension = extension - ?.addPrefix(".") - ?: "" return SingleResourceContainer( - Url("publication$dottedExtension")!!, + Url(extension.appendToFilename("publication"))!!, resource ) }