From 09e338b0f3acc8d59282280bded6c4bf93de6281 Mon Sep 17 00:00:00 2001 From: qnga <32197639+qnga@users.noreply.github.com> Date: Wed, 10 Jan 2024 13:05:14 +0100 Subject: [PATCH] Merge `AssetOpener` and `AssetSniffer` public APIs into `AssetRetriever` (#434) --- docs/guides/open-publication.md | 27 +- .../readium/r2/lcp/LcpContentProtection.kt | 16 +- .../readium/r2/lcp/LcpPublicationRetriever.kt | 6 +- .../java/org/readium/r2/lcp/LcpService.kt | 6 +- .../readium/r2/lcp/service/LicensesService.kt | 8 +- .../util/{asset => archive}/ArchiveOpener.kt | 35 ++- .../ArchiveProperties.kt | 3 +- .../org/readium/r2/shared/util/asset/Asset.kt | 8 +- .../r2/shared/util/asset/AssetOpener.kt | 145 --------- .../r2/shared/util/asset/AssetRetriever.kt | 292 ++++++++++++++++++ .../r2/shared/util/asset/AssetSniffer.kt | 72 ++--- .../readium/r2/shared/util/asset/Defaults.kt | 2 + .../r2/shared/util/asset/SniffError.kt | 22 -- .../shared/util/zip/FileZipArchiveProvider.kt | 11 +- .../r2/shared/util/zip/FileZipContainer.kt | 4 +- .../util/zip/StreamingZipArchiveProvider.kt | 9 +- .../shared/util/zip/StreamingZipContainer.kt | 4 +- .../r2/shared/util/zip/ZipArchiveOpener.kt | 5 +- .../r2/shared/util/asset/AssetSnifferTest.kt | 31 +- .../r2/shared/util/resource/PropertiesTest.kt | 2 + .../r2/streamer/extensions/Container.kt | 11 +- .../r2/streamer/parser/PublicationParser.kt | 10 +- .../r2/streamer/parser/audio/AudioParser.kt | 4 +- .../parser/epub/EpubPositionsService.kt | 2 +- .../r2/streamer/parser/image/ImageParser.kt | 6 +- .../parser/epub/EpubPositionsServiceTest.kt | 4 +- .../streamer/parser/image/ImageParserTest.kt | 8 +- .../org/readium/r2/testapp/Application.kt | 4 +- .../java/org/readium/r2/testapp/Readium.kt | 10 +- .../readium/r2/testapp/domain/Bookshelf.kt | 6 +- .../r2/testapp/domain/PublicationError.kt | 18 +- .../r2/testapp/domain/PublicationRetriever.kt | 6 +- .../r2/testapp/reader/ReaderRepository.kt | 2 +- 33 files changed, 463 insertions(+), 336 deletions(-) rename readium/shared/src/main/java/org/readium/r2/shared/util/{asset => archive}/ArchiveOpener.kt (71%) rename readium/shared/src/main/java/org/readium/r2/shared/util/{resource => archive}/ArchiveProperties.kt (95%) delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt create mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt delete mode 100644 readium/shared/src/main/java/org/readium/r2/shared/util/asset/SniffError.kt diff --git a/docs/guides/open-publication.md b/docs/guides/open-publication.md index ccba4a28c3..e799043423 100644 --- a/docs/guides/open-publication.md +++ b/docs/guides/open-publication.md @@ -4,23 +4,23 @@ Readium requires you to instantiate a few components before you can actually open a publication. -## Constructing an `AssetOpener` +## Constructing an `AssetRetriever` First, you need to instantiate an `HttpClient` to provide the toolkit the ability to do HTTP requests. You can use the Readium `DefaultHttpClient` or a custom implementation. In the former case, its callback will enable you to perform authentication when required. -Then, you can create an `AssetOpener` which will enable you to read content through different schemes and guessing its format. -In addition to an `HttpClient`, the `AssetOpener` constructor takes a `ContentResolver` to support data access through the `content` scheme. +Then, you can create an `AssetRetriever` which will enable you to read content through different schemes and guess its format. +In addition to an `HttpClient`, the `AssetRetriever` constructor takes a `ContentResolver` to support data access through the `content` scheme. ```kotlin val httpClient = DefaultHttpClient() -val assetOpener = AssetOpener(context.contentResolver, httpClient) +val assetRetriever = AssetRetriever(context.contentResolver, httpClient) ``` ## Constructing a `PublicationOpener` -The component which can parse an `Asset` giving access to a publication to build a proper `Publication` +The component which can parse an `Asset` giving access to a publication to build a `Publication` object is the `PublicationOpener`. Its constructor requires you to pass in: * a `PublicationParser` it delegates the parsing work to. @@ -32,38 +32,39 @@ The easiest way to get a `PublicationParser` is to use the `DefaultPublicationPa ```kotlin val contentProtections = listOf(lcpService.contentProtection(authentication)) -val publicationParser = DefaultPublicationParser(context, httpClient, assetOpener, pdfFactory) +val publicationParser = DefaultPublicationParser(context, httpClient, assetRetriever, pdfFactory) val publicationOpener = PublicationOpener(publicationParser, contentProtections) ``` ## Bringing the pieces together -Once you have got an `AssetOpener` and a `PublicationOpener`, you can eventually open a publication as follows: +Once you have got an `AssetRetriever` and a `PublicationOpener`, you can eventually open a publication as follows: ```kotlin -val asset = assetOpener.open(url, mediaType) +val asset = assetRetriever.open(url, mediaType) .getOrElse { return error } val publication = publicationOpener.open(asset) .getOrElse { return error } ``` -Persisting the asset media type on the device can significantly improve performance as it is valuable hint +Persisting the asset media type on the device can significantly improve performance as it is strong hint for the content format, especially in case of remote publications. -## Extensibility` +## Supporting additional formats or URL schemes `DefaultPublicationParser` accepts additional parsers. You can also use your own parser list with `CompositePublicationParser` or implement [PublicationParser] in the way you like. -`AssetOpener` offers an alternative constructor providing better extensibility in a similar way. +`AssetRetriever` offers an alternative constructor providing better extensibility in a similar way. This constructor takes several parameters with different responsibilities. * `ResourceFactory` determines which schemes you will be able to access content through. -* `ArchiveOpener` which kinds of archives your `AssetOpener` will be able to open. -* `FormatSniffer` which file formats your `AssetOpener` will be able to identify. +* `ArchiveOpener` which kinds of archives your `AssetRetriever` will be able to open. +* `FormatSniffer` which file formats your `AssetRetriever` will be able to identify. For each of these components, you can either use the default implementations or implement yours with the composite pattern. `CompositeResourceFactory`, `CompositeArchiveOpener` and `CompositeFormatSniffer` provide simple implementations trying every item of a list in turns. + 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 39c41ed4a8..6d038707d8 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 @@ -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.asset.Asset -import org.readium.r2.shared.util.asset.AssetOpener +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.Container @@ -38,7 +38,7 @@ import org.readium.r2.shared.util.resource.TransformingContainer internal class LcpContentProtection( private val lcpService: LcpService, private val authentication: LcpAuthenticating, - private val assetOpener: AssetOpener + private val assetRetriever: AssetRetriever ) : ContentProtection { override suspend fun open( @@ -191,14 +191,14 @@ internal class LcpContentProtection( val asset = if (link.mediaType != null) { - assetOpener.open( + assetRetriever.retrieve( url, mediaType = link.mediaType ) .map { it as ContainerAsset } .mapFailure { it.wrap() } } else { - assetOpener.open(url) + assetRetriever.retrieve(url) .mapFailure { it.wrap() } .flatMap { if (it is ContainerAsset) { @@ -218,13 +218,13 @@ internal class LcpContentProtection( return asset.flatMap { createResultAsset(it, license) } } - private fun AssetOpener.OpenError.wrap(): ContentProtection.OpenError = + private fun AssetRetriever.RetrieveUrlError.wrap(): ContentProtection.OpenError = when (this) { - is AssetOpener.OpenError.FormatNotSupported -> + is AssetRetriever.RetrieveUrlError.FormatNotSupported -> ContentProtection.OpenError.AssetNotSupported(this) - is AssetOpener.OpenError.Reading -> + is AssetRetriever.RetrieveUrlError.Reading -> ContentProtection.OpenError.Reading(cause) - is AssetOpener.OpenError.SchemeNotSupported -> + is AssetRetriever.RetrieveUrlError.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 2cafc8fcc5..127f707d4c 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 @@ -16,7 +16,7 @@ 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.FileExtension -import org.readium.r2.shared.util.asset.AssetSniffer +import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.format.EpubSpecification import org.readium.r2.shared.util.format.Format @@ -33,7 +33,7 @@ import org.readium.r2.shared.util.mediatype.MediaType public class LcpPublicationRetriever( context: Context, private val downloadManager: DownloadManager, - private val assetSniffer: AssetSniffer + private val assetRetriever: AssetRetriever ) { @JvmInline @@ -197,7 +197,7 @@ public class LcpPublicationRetriever( downloadsRepository.removeDownload(requestId.value) val format = - assetSniffer.sniff( + assetRetriever.sniffFormat( download.file, FormatHints( mediaTypes = listOfNotNull( 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 daea68954e..23222a2d27 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,7 +29,7 @@ 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.AssetOpener +import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.downloads.DownloadManager import org.readium.r2.shared.util.format.Format @@ -174,7 +174,7 @@ public interface LcpService { */ public operator fun invoke( context: Context, - assetOpener: AssetOpener, + assetRetriever: AssetRetriever, downloadManager: DownloadManager ): LcpService? { if (!LcpClient.isAvailable()) { @@ -200,7 +200,7 @@ public interface LcpService { network = network, passphrases = passphrases, context = context, - assetOpener = assetOpener, + assetRetriever = assetRetriever, downloadManager = downloadManager ) } 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 9943d865aa..aabdbf8b5d 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 @@ -33,7 +33,7 @@ import org.readium.r2.lcp.license.model.LicenseDocument 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.AssetOpener +import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.downloads.DownloadManager import timber.log.Timber @@ -44,20 +44,20 @@ internal class LicensesService( private val network: NetworkService, private val passphrases: PassphrasesService, private val context: Context, - private val assetOpener: AssetOpener, + private val assetRetriever: AssetRetriever, private val downloadManager: DownloadManager ) : LcpService, CoroutineScope by MainScope() { override fun contentProtection( authentication: LcpAuthenticating ): ContentProtection = - LcpContentProtection(this, authentication, assetOpener) + LcpContentProtection(this, authentication, assetRetriever) override fun publicationRetriever(): LcpPublicationRetriever { return LcpPublicationRetriever( context, downloadManager, - assetOpener.assetSniffer + assetRetriever ) } 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/archive/ArchiveOpener.kt similarity index 71% rename from readium/shared/src/main/java/org/readium/r2/shared/util/asset/ArchiveOpener.kt rename to readium/shared/src/main/java/org/readium/r2/shared/util/archive/ArchiveOpener.kt index 5dd94049a3..13da05b50d 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/archive/ArchiveOpener.kt @@ -4,10 +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.archive +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.Try +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.data.Readable import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.getOrElse @@ -20,19 +23,31 @@ 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 { + override val cause: Error? + ) : Error { public class FormatNotSupported( public val format: Format, - cause: org.readium.r2.shared.util.Error? = null + cause: Error? = null ) : OpenError("Format not supported.", cause) public class Reading( - override val cause: org.readium.r2.shared.util.data.ReadError + override val cause: ReadError ) : OpenError("An error occurred while attempting to read the resource.", cause) } + public sealed class SniffOpenError( + override val message: String, + override val cause: Error? + ) : Error { + + public data object NotRecognized : + SniffOpenError("Format of resource could not be inferred.", null) + + public data class Reading(override val cause: ReadError) : + SniffOpenError("An error occurred while trying to read content.", cause) + } + /** * Creates a new [Container] to access the entries of an archive with a known format. */ @@ -46,7 +61,7 @@ public interface ArchiveOpener { */ public suspend fun sniffOpen( source: Readable - ): Try + ): Try } /** @@ -78,18 +93,20 @@ public class CompositeArchiveOpener( return Try.failure(ArchiveOpener.OpenError.FormatNotSupported(format)) } - override suspend fun sniffOpen(source: Readable): Try { + override suspend fun sniffOpen( + source: Readable + ): Try { for (factory in openers) { factory.sniffOpen(source) .getOrElse { error -> when (error) { - is SniffError.NotRecognized -> null + is ArchiveOpener.SniffOpenError.NotRecognized -> null else -> return Try.failure(error) } } ?.let { return Try.success(it) } } - return Try.failure(SniffError.NotRecognized) + return Try.failure(ArchiveOpener.SniffOpenError.NotRecognized) } } 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/asset/Asset.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Asset.kt index 19e56bfc5d..95dd31e2ba 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,6 +6,7 @@ package org.readium.r2.shared.util.asset +import org.readium.r2.shared.util.SuspendingCloseable import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.resource.Resource @@ -13,17 +14,12 @@ import org.readium.r2.shared.util.resource.Resource /** * An asset which is either a single resource or a container that holds multiple resources. */ -public sealed class Asset { +public sealed class Asset : SuspendingCloseable { /** * Format of the asset. */ public abstract val format: Format - - /** - * Releases in-memory resources related to this asset. - */ - public abstract suspend fun close() } /** 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 deleted file mode 100644 index b93e1b790f..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetOpener.kt +++ /dev/null @@ -1,145 +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 android.content.ContentResolver -import java.io.File -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.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.format.FormatSniffer -import org.readium.r2.shared.util.getOrElse -import org.readium.r2.shared.util.http.HttpClient -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 - -/** - * 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( - @InternalReadiumApi public val assetSniffer: AssetSniffer, - private val resourceFactory: ResourceFactory, - private val archiveOpener: ArchiveOpener -) { - public constructor( - resourceFactory: ResourceFactory, - archiveOpener: ArchiveOpener, - formatSniffer: FormatSniffer - ) : this(AssetSniffer(formatSniffer, archiveOpener), resourceFactory, archiveOpener) - - public constructor( - contentResolver: ContentResolver, - httpClient: HttpClient - ) : this( - DefaultResourceFactory(contentResolver, httpClient), - DefaultArchiveOpener(), - DefaultFormatSniffer() - ) - - 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 asset = 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(asset) - } - - 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) - } - ) - } - - 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 new file mode 100644 index 0000000000..18ad90519f --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/AssetRetriever.kt @@ -0,0 +1,292 @@ +/* + * 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 java.io.File +import org.readium.r2.shared.util.AbsoluteUrl +import org.readium.r2.shared.util.Either +import org.readium.r2.shared.util.Error +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.archive.ArchiveOpener +import org.readium.r2.shared.util.data.Container +import org.readium.r2.shared.util.file.FileResource +import org.readium.r2.shared.util.format.Format +import org.readium.r2.shared.util.format.FormatHints +import org.readium.r2.shared.util.format.FormatSniffer +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.http.HttpClient +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.resource.borrow +import org.readium.r2.shared.util.resource.filename +import org.readium.r2.shared.util.resource.mediaType +import org.readium.r2.shared.util.use + +/** + * Retrieves an [Asset] instance providing reading access to the resource(s) of an asset stored at + * a given [Url] as well as its [Format]. + */ +public class AssetRetriever private constructor( + private val assetSniffer: AssetSniffer, + private val resourceFactory: ResourceFactory, + private val archiveOpener: ArchiveOpener +) { + public constructor( + resourceFactory: ResourceFactory, + archiveOpener: ArchiveOpener, + formatSniffer: FormatSniffer + ) : this(AssetSniffer(formatSniffer, archiveOpener), resourceFactory, archiveOpener) + + public constructor( + contentResolver: ContentResolver, + httpClient: HttpClient + ) : this( + DefaultResourceFactory(contentResolver, httpClient), + DefaultArchiveOpener(), + DefaultFormatSniffer() + ) + + /** + * Error while trying to retrieve an asset from an URL. + */ + public sealed class RetrieveUrlError( + override val message: String, + override val cause: Error? + ) : Error { + + /** + * The scheme (e.g. http, file, content) for the requested [Url] is not supported. + */ + public class SchemeNotSupported( + public val scheme: Url.Scheme, + cause: Error? = null + ) : RetrieveUrlError("Url scheme $scheme is not supported.", cause) + + /** + * The format of the resource at the requested [Url] is not recognized. + */ + public class FormatNotSupported( + cause: Error? = null + ) : RetrieveUrlError("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) : + RetrieveUrlError("An error occurred when trying to read asset.", cause) + } + + /** + * Error while trying to retrieve an asset from a [Resource] or a [Container]. + */ + public sealed class RetrieveError( + override val message: String, + override val cause: Error? + ) : Error { + + /** + * The format of the resource is not recognized. + */ + public class FormatNotSupported( + cause: Error? = null + ) : 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) + } + + /** + * Retrieves an asset from an url and a known format. + */ + public suspend fun retrieve( + url: AbsoluteUrl, + format: Format + ): Try { + val resource = resourceFactory.create(url) + .getOrElse { + when (it) { + is ResourceFactory.Error.SchemeNotSupported -> + return Try.failure(RetrieveUrlError.SchemeNotSupported(it.scheme, it)) + } + } + + val asset = archiveOpener + .open(format, resource) + .getOrElse { + return when (it) { + is ArchiveOpener.OpenError.Reading -> + Try.failure(RetrieveUrlError.Reading(it.cause)) + is ArchiveOpener.OpenError.FormatNotSupported -> + Try.success(ResourceAsset(format, resource)) + } + } + + return Try.success(asset) + } + + /** + * Retrieves an asset from a local file. + */ + public suspend fun retrieve( + file: File, + formatHints: FormatHints = FormatHints() + ): Try = + retrieve(FileResource(file), formatHints) + + /** + * Retrieves an asset from an [AbsoluteUrl]. + */ + public suspend fun retrieve( + url: AbsoluteUrl, + formatHints: FormatHints = FormatHints() + ): Try { + val resource = resourceFactory.create(url) + .getOrElse { + return Try.failure( + when (it) { + is ResourceFactory.Error.SchemeNotSupported -> + RetrieveUrlError.SchemeNotSupported(it.scheme) + } + ) + } + + return retrieve(resource, formatHints) + .mapFailure { + when (it) { + is RetrieveError.FormatNotSupported -> RetrieveUrlError.FormatNotSupported( + it.cause + ) + is RetrieveError.Reading -> RetrieveUrlError.Reading(it.cause) + } + } + } + + /** + * Retrieves an asset from an [AbsoluteUrl]. + */ + public suspend fun retrieve( + url: AbsoluteUrl, + mediaType: MediaType + ): Try = + retrieve(url, FormatHints(mediaType = mediaType)) + + /** + * Retrieves an asset from a local file. + */ + public suspend fun retrieve( + file: File, + mediaType: MediaType + ): Try = + retrieve(file, FormatHints(mediaType = mediaType)) + + /** + * Retrieves an asset from an already opened resource. + */ + public suspend fun retrieve( + resource: Resource, + hints: FormatHints = FormatHints() + ): Try { + val properties = resource.properties() + .getOrElse { return Try.failure(RetrieveError.Reading(it)) } + + val internalHints = FormatHints( + mediaType = properties.mediaType, + fileExtension = properties.filename + ?.substringAfterLast(".") + ?.let { FileExtension((it)) } + ) + + return assetSniffer + .sniff(Either.Left(resource), hints + internalHints) + .mapFailure { + when (it) { + AssetSniffer.SniffError.NotRecognized -> RetrieveError.FormatNotSupported(it) + is AssetSniffer.SniffError.Reading -> RetrieveError.Reading(it.cause) + } + } + } + + /** + * Retrieves an asset from an already opened container. + */ + public suspend fun retrieve( + container: Container, + hints: FormatHints = FormatHints() + ): Try = + assetSniffer + .sniff(Either.Right(container), hints) + .mapFailure { + when (it) { + AssetSniffer.SniffError.NotRecognized -> RetrieveError.FormatNotSupported(it) + is AssetSniffer.SniffError.Reading -> RetrieveError.Reading(it.cause) + } + } + + /** + * Retrieves an asset from an already opened resource. + */ + public suspend fun retrieve( + resource: Resource, + mediaType: MediaType + ): Try = + retrieve(resource, FormatHints(mediaType = mediaType)) + + /** + * Retrieves an asset from an already opened container. + */ + public suspend fun retrieve( + container: Container, + mediaType: MediaType + ): Try = + retrieve(container, FormatHints(mediaType = mediaType)) + + /** + * Sniffs the format of a file content. + */ + public suspend fun sniffFormat( + file: File, + hints: FormatHints = FormatHints() + ): Try = + FileResource(file).use { sniffFormat(it, hints) } + + /** + * Sniffs the format of the content available at [url]. + */ + public suspend fun sniffFormat( + url: AbsoluteUrl, + hints: FormatHints = FormatHints() + ): Try = + retrieve(url, hints) + .map { asset -> asset.use { it.format } } + + /** + * Sniffs the format of a resource content. + */ + public suspend fun sniffFormat( + resource: Resource, + hints: FormatHints = FormatHints() + ): Try = + retrieve(resource.borrow(), hints) + .map { asset -> asset.use { it.format } } + + /** + * Sniffs the format of a container content. + */ + public suspend fun sniffFormat( + container: Container, + hints: FormatHints = FormatHints() + ): Try = + retrieve(container, hints) + .map { asset -> asset.use { it.format } } +} 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 b7a6946dbc..212a020500 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 @@ -6,16 +6,16 @@ package org.readium.r2.shared.util.asset -import java.io.File import org.readium.r2.shared.util.Either +import org.readium.r2.shared.util.Error import org.readium.r2.shared.util.FileExtension import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.archive.ArchiveOpener 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.Format import org.readium.r2.shared.util.format.FormatHints import org.readium.r2.shared.util.format.FormatSniffer @@ -23,60 +23,26 @@ import org.readium.r2.shared.util.format.FormatSpecification 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.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 -public class AssetSniffer( - private val formatSniffers: FormatSniffer = DefaultFormatSniffer(), - private val archiveOpener: ArchiveOpener = ZipArchiveOpener() +internal class AssetSniffer( + private val formatSniffer: FormatSniffer = DefaultFormatSniffer(), + private val archiveOpener: ArchiveOpener = DefaultArchiveOpener() ) { - public suspend fun sniff( - file: File, - hints: FormatHints = FormatHints() - ): Try = - FileResource(file).use { sniff(it, hints) } - - public suspend fun sniff( - resource: Resource, - hints: FormatHints = FormatHints() - ): Try = - sniffOpen(resource.borrow(), hints).map { it.format } - - public suspend fun sniff( - container: Container, - hints: FormatHints = FormatHints() - ): Try = - 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)) } + sealed class SniffError( + override val message: String, + override val cause: Error? + ) : Error { - val internalHints = FormatHints( - mediaType = properties.mediaType, - fileExtension = properties.filename - ?.substringAfterLast(".") - ?.let { FileExtension((it)) } - ) + data object NotRecognized : + SniffError("Format of resource could not be inferred.", null) - return sniff(Either.Left(resource), hints + internalHints) + data class Reading(override val cause: ReadError) : + SniffError("An error occurred while trying to read content.", cause) } - public suspend fun sniffOpen( - file: File, - hints: FormatHints = FormatHints() - ): Try = - sniff(Either.Left(FileResource(file)), hints) - - private suspend fun sniff( + suspend fun sniff( source: Either>, hints: FormatHints ): Try { @@ -106,7 +72,7 @@ public class AssetSniffer( cache: Either>, hints: FormatHints ): Try { - formatSniffers + formatSniffer .sniffHints(format, hints) .takeIf { it.conformsTo(format) } ?.takeIf { it != format } @@ -114,7 +80,7 @@ public class AssetSniffer( when (cache) { is Either.Left -> - formatSniffers + formatSniffer .sniffBlob(format, cache.value) .getOrElse { return Try.failure(it) } .takeIf { it.conformsTo(format) } @@ -122,7 +88,7 @@ public class AssetSniffer( ?.let { return doSniff(it, source, cache, hints) } is Either.Right -> - formatSniffers + formatSniffer .sniffContainer(format, cache.value) .getOrElse { return Try.failure(it) } .takeIf { it.conformsTo(format) } @@ -159,9 +125,9 @@ public class AssetSniffer( archiveOpener.sniffOpen(source) .tryRecover { when (it) { - is SniffError.NotRecognized -> + is ArchiveOpener.SniffOpenError.NotRecognized -> Try.success(null) - is SniffError.Reading -> + is ArchiveOpener.SniffOpenError.Reading -> Try.failure(it.cause) } } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Defaults.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Defaults.kt index 93e11be884..62f3397d45 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Defaults.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/asset/Defaults.kt @@ -7,6 +7,8 @@ package org.readium.r2.shared.util.asset import android.content.ContentResolver +import org.readium.r2.shared.util.archive.ArchiveOpener +import org.readium.r2.shared.util.archive.CompositeArchiveOpener import org.readium.r2.shared.util.content.ContentResourceFactory import org.readium.r2.shared.util.file.FileResourceFactory import org.readium.r2.shared.util.format.ArchiveSniffer 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 deleted file mode 100644 index 6539ea17fd..0000000000 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/asset/SniffError.kt +++ /dev/null @@ -1,22 +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.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/zip/FileZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/FileZipArchiveProvider.kt index b725251086..4989b5c5dc 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,8 +14,7 @@ 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.asset.ArchiveOpener -import org.readium.r2.shared.util.asset.SniffError +import org.readium.r2.shared.util.archive.ArchiveOpener import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.file.FileSystemError @@ -28,22 +27,22 @@ import org.readium.r2.shared.util.resource.Resource */ internal class FileZipArchiveProvider { - suspend fun sniffOpen(file: File): Try, SniffError> { + suspend fun sniffOpen(file: File): Try, ArchiveOpener.SniffOpenError> { return withContext(Dispatchers.IO) { try { val container = FileZipContainer(ZipFile(file), file) Try.success(container) } catch (e: ZipException) { - Try.failure(SniffError.NotRecognized) + Try.failure(ArchiveOpener.SniffOpenError.NotRecognized) } catch (e: SecurityException) { Try.failure( - SniffError.Reading( + ArchiveOpener.SniffOpenError.Reading( ReadError.Access(FileSystemError.Forbidden(e)) ) ) } catch (e: IOException) { Try.failure( - SniffError.Reading( + ArchiveOpener.SniffOpenError.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 a2f6c7b2ed..7b175ebad5 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,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.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 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 6050a747bb..cdc63c5864 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,8 +13,7 @@ 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.asset.ArchiveOpener -import org.readium.r2.shared.util.asset.SniffError +import org.readium.r2.shared.util.archive.ArchiveOpener import org.readium.r2.shared.util.data.Container import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.data.ReadException @@ -32,14 +31,14 @@ import org.readium.r2.shared.util.zip.jvm.SeekableByteChannel */ internal class StreamingZipArchiveProvider { - suspend fun sniffOpen(source: Readable): Try, SniffError> { + suspend fun sniffOpen(source: Readable): Try, ArchiveOpener.SniffOpenError> { return try { val container = openBlob(source, ::ReadException, null) Try.success(container) } catch (exception: Exception) { exception.findInstance(ReadException::class.java) - ?.let { Try.failure(SniffError.Reading(it.error)) } - ?: Try.failure(SniffError.NotRecognized) + ?.let { Try.failure(ArchiveOpener.SniffOpenError.Reading(it.error)) } + ?: Try.failure(ArchiveOpener.SniffOpenError.NotRecognized) } } 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 730cdfae23..584183d606 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,15 +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.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 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 e15d92d44c..84f4f56c06 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 @@ -8,9 +8,8 @@ package org.readium.r2.shared.util.zip import org.readium.r2.shared.util.FileExtension import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.ArchiveOpener +import org.readium.r2.shared.util.archive.ArchiveOpener import org.readium.r2.shared.util.asset.ContainerAsset -import org.readium.r2.shared.util.asset.SniffError import org.readium.r2.shared.util.data.Readable import org.readium.r2.shared.util.format.Format import org.readium.r2.shared.util.format.FormatSpecification @@ -37,7 +36,7 @@ public class ZipArchiveOpener : ArchiveOpener { override suspend fun sniffOpen( source: Readable - ): Try { + ): Try { val container = (source as? Resource)?.sourceUrl?.toFile() ?.let { fileZipArchiveProvider.sniffOpen(it) } ?: streamingZipArchiveProvider.sniffOpen(source) diff --git a/readium/shared/src/test/java/org/readium/r2/shared/util/asset/AssetSnifferTest.kt b/readium/shared/src/test/java/org/readium/r2/shared/util/asset/AssetSnifferTest.kt index 77ac1cc76e..1f0ca9a491 100644 --- a/readium/shared/src/test/java/org/readium/r2/shared/util/asset/AssetSnifferTest.kt +++ b/readium/shared/src/test/java/org/readium/r2/shared/util/asset/AssetSnifferTest.kt @@ -6,15 +6,18 @@ package org.readium.r2.shared.util.asset +import java.io.File 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.Either import org.readium.r2.shared.util.FileExtension import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.checkSuccess import org.readium.r2.shared.util.data.EmptyContainer +import org.readium.r2.shared.util.file.FileResource import org.readium.r2.shared.util.format.AvifSpecification import org.readium.r2.shared.util.format.BmpSpecification import org.readium.r2.shared.util.format.EpubSpecification @@ -48,6 +51,7 @@ import org.readium.r2.shared.util.format.WebpSpecification import org.readium.r2.shared.util.format.XmlSpecification import org.readium.r2.shared.util.format.ZipSpecification 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.robolectric.RobolectricTestRunner @@ -58,16 +62,21 @@ class AssetSnifferTest { private val sniffer = AssetSniffer() - private suspend fun AssetSniffer.sniffHints(formatHints: FormatHints): Try = - sniff( - hints = formatHints, - container = EmptyContainer() - ) + private suspend fun AssetSniffer.sniffHints(formatHints: FormatHints): Try = + sniff(hints = formatHints, source = Either.Right(EmptyContainer())) + .map { it.format } + + private suspend fun AssetSniffer.sniff(file: File, hints: FormatHints = FormatHints()): Try = + sniff(FileResource(file), hints) + + private suspend fun AssetSniffer.sniff(resource: Resource, hints: FormatHints = FormatHints()): Try = + sniff(hints = hints, source = Either.Left(resource)) + .map { it.format } - private suspend fun AssetSniffer.sniffFileExtension(extension: String?): Try = + private suspend fun AssetSniffer.sniffFileExtension(extension: String?): Try = sniffHints(FormatHints(fileExtension = extension?.let { FileExtension((it)) })) - private suspend fun AssetSniffer.sniffMediaType(mediaType: String?): Try = + private suspend fun AssetSniffer.sniffMediaType(mediaType: String?): Try = sniffHints(FormatHints(mediaType = mediaType?.let { MediaType(it) })) private val epubFormat = @@ -119,7 +128,7 @@ class AssetSnifferTest { fun `sniff from metadata`() = runBlocking { assertEquals( sniffer.sniffFileExtension(null).failureOrNull(), - SniffError.NotRecognized + AssetSniffer.SniffError.NotRecognized ) assertEquals( audiobookFormat, @@ -127,7 +136,7 @@ class AssetSnifferTest { ) assertEquals( sniffer.sniffMediaType(null).failureOrNull(), - SniffError.NotRecognized + AssetSniffer.SniffError.NotRecognized ) assertEquals( audiobookFormat, @@ -155,11 +164,11 @@ class AssetSnifferTest { @Test fun `sniff unknown format`() = runBlocking { assertEquals( - SniffError.NotRecognized, + AssetSniffer.SniffError.NotRecognized, sniffer.sniffMediaType(mediaType = "invalid").failureOrNull() ) assertEquals( - SniffError.NotRecognized, + AssetSniffer.SniffError.NotRecognized, sniffer.sniff(fixtures.fileAt("unknown")).failureOrNull() ) } 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 901ee8efde..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) 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 67fef11928..bf079a4814 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 @@ -13,9 +13,8 @@ import java.io.File 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.AssetRetriever 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 @@ -56,7 +55,7 @@ internal fun ResourceAsset.toContainer(): Container { ) } -internal suspend fun AssetSniffer.sniffContainerEntries( +internal suspend fun AssetRetriever.sniffContainerEntries( container: Container, filter: (Url) -> Boolean ): Try, ReadError> = @@ -69,14 +68,14 @@ internal suspend fun AssetSniffer.sniffContainerEntries( is Try.Success -> container[url]!!.use { resource -> - sniff(resource).fold( + sniffFormat(resource).fold( onSuccess = { Try.success(acc.value + (url to it)) }, onFailure = { when (it) { - SniffError.NotRecognized -> acc - is SniffError.Reading -> Try.failure(it.cause) + is AssetRetriever.RetrieveError.FormatNotSupported -> acc + is AssetRetriever.RetrieveError.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 1dc9a0da78..026bd23397 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 @@ -10,7 +10,7 @@ import android.content.Context import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.asset.Asset -import org.readium.r2.shared.util.asset.AssetOpener +import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.http.HttpClient import org.readium.r2.shared.util.logging.WarningLogger import org.readium.r2.shared.util.pdf.PdfDocumentFactory @@ -58,12 +58,12 @@ public interface PublicationParser { * @param additionalParsers Parsers used to open a publication, in addition to the default parsers. They take precedence over the default ones. * @param httpClient Service performing HTTP requests. * @param pdfFactory Parses a PDF document, optionally protected by password. - * @param assetOpener Opens assets in case of indirection. + * @param assetRetriever Opens assets in case of indirection. */ public class DefaultPublicationParser( context: Context, private val httpClient: HttpClient, - assetOpener: AssetOpener, + assetRetriever: AssetRetriever, pdfFactory: PdfDocumentFactory<*>?, additionalParsers: List = emptyList() ) : PublicationParser by CompositePublicationParser( @@ -71,8 +71,8 @@ public class DefaultPublicationParser( EpubParser(), pdfFactory?.let { PdfParser(context, it) }, ReadiumWebPubParser(context, httpClient, pdfFactory), - ImageParser(assetOpener.assetSniffer), - AudioParser(assetOpener.assetSniffer) + ImageParser(assetRetriever), + AudioParser(assetRetriever) ) ) 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 9a90fa1220..1ccb4cadbc 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,7 +15,7 @@ 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.AssetRetriever import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.data.Container @@ -48,7 +48,7 @@ import org.readium.r2.streamer.parser.PublicationParser * It can also work for a standalone audio file. */ public class AudioParser( - private val assetSniffer: AssetSniffer + private val assetSniffer: AssetRetriever ) : PublicationParser { override suspend fun parse( 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 2ae7ce2858..9a31fcc2b7 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,7 +16,7 @@ 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.AssetRetriever import org.readium.r2.shared.util.asset.ContainerAsset import org.readium.r2.shared.util.asset.ResourceAsset import org.readium.r2.shared.util.data.Container @@ -49,7 +49,7 @@ import org.readium.r2.streamer.parser.PublicationParser * It can also work for a standalone bitmap file. */ public class ImageParser( - private val assetSniffer: AssetSniffer + private val assetRetriever: AssetRetriever ) : PublicationParser { override suspend fun parse( @@ -84,7 +84,7 @@ public class ImageParser( return Try.failure(PublicationParser.ParseError.FormatNotSupported()) } - val entryFormats: Map = assetSniffer + val entryFormats: Map = assetRetriever .sniffContainerEntries(asset.container) { !it.isHiddenOrThumbs } .getOrElse { return Try.failure(PublicationParser.ParseError.Reading(it)) } 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 48f28c7591..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 @@ -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.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.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 6d98cde8e7..6fee0b8388 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,7 +20,7 @@ import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.firstWithRel import org.readium.r2.shared.util.FileExtension import org.readium.r2.shared.util.Url -import org.readium.r2.shared.util.asset.AssetSniffer +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.checkSuccess @@ -30,17 +30,21 @@ import org.readium.r2.shared.util.format.FormatSpecification import org.readium.r2.shared.util.format.InformalComicSpecification import org.readium.r2.shared.util.format.JpegSpecification import org.readium.r2.shared.util.format.ZipSpecification +import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.zip.ZipArchiveOpener import org.readium.r2.streamer.parseBlocking import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment @RunWith(RobolectricTestRunner::class) class ImageParserTest { private val archiveOpener = ZipArchiveOpener() - private val assetSniffer = AssetSniffer() + private val contentResolver = RuntimeEnvironment.getApplication().contentResolver + + private val assetSniffer = AssetRetriever(contentResolver, DefaultHttpClient()) private val parser = ImageParser(assetSniffer) 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 f1faab5ef1..be3dca948a 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 @@ -77,7 +77,7 @@ class Application : android.app.Application() { bookRepository, CoverStorage(storageDir, httpClient = readium.httpClient), readium.publicationOpener, - readium.assetOpener, + readium.assetRetriever, createPublicationRetriever = { listener -> PublicationRetriever( listener = listener, @@ -86,7 +86,7 @@ class Application : android.app.Application() { listener = localListener, context = applicationContext, storageDir = storageDir, - assetOpener = readium.assetOpener, + assetRetriever = readium.assetRetriever, createLcpPublicationRetriever = { lcpListener -> readium.lcpService.getOrNull()?.publicationRetriever() ?.let { retriever -> 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 ca724f339a..5e1d029a4b 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 @@ -16,7 +16,7 @@ import org.readium.r2.navigator.preferences.FontFamily import org.readium.r2.shared.ExperimentalReadiumApi import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.AssetOpener +import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.downloads.android.AndroidDownloadManager import org.readium.r2.shared.util.http.DefaultHttpClient import org.readium.r2.streamer.PublicationOpener @@ -30,8 +30,8 @@ class Readium(context: Context) { val httpClient = DefaultHttpClient() - val assetOpener = - AssetOpener(context.contentResolver, httpClient) + val assetRetriever = + AssetRetriever(context.contentResolver, httpClient) val downloadManager = AndroidDownloadManager( @@ -45,7 +45,7 @@ class Readium(context: Context) { */ val lcpService = LcpService( context, - assetOpener, + assetRetriever, downloadManager )?.let { Try.success(it) } ?: Try.failure(LcpError.Unknown(DebugError("liblcp is missing on the classpath"))) @@ -62,7 +62,7 @@ class Readium(context: Context) { val publicationOpener = PublicationOpener( publicationParser = DefaultPublicationParser( context, - assetOpener = assetOpener, + assetRetriever = assetRetriever, 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/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index ba97ce1197..39246187e5 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.util.AbsoluteUrl import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try -import org.readium.r2.shared.util.asset.AssetOpener +import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.file.FileSystemError import org.readium.r2.shared.util.getOrElse import org.readium.r2.shared.util.toUrl @@ -37,7 +37,7 @@ class Bookshelf( private val bookRepository: BookRepository, private val coverStorage: CoverStorage, private val publicationOpener: PublicationOpener, - private val assetOpener: AssetOpener, + private val assetRetriever: AssetRetriever, createPublicationRetriever: (PublicationRetriever.Listener) -> PublicationRetriever ) { val channel: Channel = @@ -122,7 +122,7 @@ class Bookshelf( coverUrl: AbsoluteUrl? = null ): Try { val asset = - assetOpener.open(url) + assetRetriever.retrieve(url) .getOrElse { return Try.failure( ImportError.Publication(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 30b5677a35..c2c6d6fd4d 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 @@ -7,7 +7,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.shared.util.asset.AssetRetriever import org.readium.r2.streamer.PublicationOpener import org.readium.r2.testapp.R import org.readium.r2.testapp.utils.UserError @@ -48,16 +48,24 @@ sealed class PublicationError( companion object { - operator fun invoke(error: AssetOpener.OpenError): PublicationError = + operator fun invoke(error: AssetRetriever.RetrieveUrlError): PublicationError = when (error) { - is AssetOpener.OpenError.Reading -> + is AssetRetriever.RetrieveUrlError.Reading -> ReadError(error.cause) - is AssetOpener.OpenError.FormatNotSupported -> + is AssetRetriever.RetrieveUrlError.FormatNotSupported -> FormatNotSupported(error) - is AssetOpener.OpenError.SchemeNotSupported -> + is AssetRetriever.RetrieveUrlError.SchemeNotSupported -> UnsupportedScheme(error) } + operator fun invoke(error: AssetRetriever.RetrieveError): PublicationError = + when (error) { + is AssetRetriever.RetrieveError.Reading -> + ReadError(error.cause) + is AssetRetriever.RetrieveError.FormatNotSupported -> + FormatNotSupported(error) + } + operator fun invoke(error: PublicationOpener.OpenError): PublicationError = when (error) { is PublicationOpener.OpenError.Reading -> 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 55948c0cf6..45b84ca90c 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 @@ -23,7 +23,7 @@ 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.AssetOpener +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 @@ -104,7 +104,7 @@ class LocalPublicationRetriever( private val listener: PublicationRetriever.Listener, private val context: Context, private val storageDir: File, - private val assetOpener: AssetOpener, + private val assetRetriever: AssetRetriever, createLcpPublicationRetriever: (PublicationRetriever.Listener) -> LcpPublicationRetriever? ) { @@ -149,7 +149,7 @@ class LocalPublicationRetriever( tempFile: File, coverUrl: AbsoluteUrl? = null ) { - val sourceAsset = assetOpener.open(tempFile) + val sourceAsset = assetRetriever.retrieve(tempFile) .getOrElse { listener.onError( ImportError.Publication(PublicationError(it)) 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 c8e8880461..c10784fa2b 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.assetOpener.open( + val asset = readium.assetRetriever.retrieve( book.url, book.mediaType ).getOrElse {