Skip to content

Commit

Permalink
Merge AssetOpener and AssetSniffer public APIs into `AssetRetriev…
Browse files Browse the repository at this point in the history
…er` (readium#434)
  • Loading branch information
qnga authored Jan 10, 2024
1 parent 1ba1223 commit 09e338b
Show file tree
Hide file tree
Showing 33 changed files with 463 additions and 336 deletions.
27 changes: 14 additions & 13 deletions docs/guides/open-publication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.


Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -197,7 +197,7 @@ public class LcpPublicationRetriever(
downloadsRepository.removeDownload(requestId.value)

val format =
assetSniffer.sniff(
assetRetriever.sniffFormat(
download.file,
FormatHints(
mediaTypes = listOfNotNull(
Expand Down
6 changes: 3 additions & 3 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,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

Expand Down Expand Up @@ -174,7 +174,7 @@ public interface LcpService {
*/
public operator fun invoke(
context: Context,
assetOpener: AssetOpener,
assetRetriever: AssetRetriever,
downloadManager: DownloadManager
): LcpService? {
if (!LcpClient.isAvailable()) {
Expand All @@ -200,7 +200,7 @@ public interface LcpService {
network = network,
passphrases = passphrases,
context = context,
assetOpener = assetOpener,
assetRetriever = assetRetriever,
downloadManager = downloadManager
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
*/
Expand All @@ -46,7 +61,7 @@ public interface ArchiveOpener {
*/
public suspend fun sniffOpen(
source: Readable
): Try<ContainerAsset, SniffError>
): Try<ContainerAsset, SniffOpenError>
}

/**
Expand Down Expand Up @@ -78,18 +93,20 @@ public class CompositeArchiveOpener(
return Try.failure(ArchiveOpener.OpenError.FormatNotSupported(format))
}

override suspend fun sniffOpen(source: Readable): Try<ContainerAsset, SniffError> {
override suspend fun sniffOpen(
source: Readable
): Try<ContainerAsset, ArchiveOpener.SniffOpenError> {
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,20 @@

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

/**
* 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()
}

/**
Expand Down
Loading

0 comments on commit 09e338b

Please sign in to comment.