Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Small changes for v3 and guide to open a publication #432

Merged
merged 7 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/guides/index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# User guides

* [Opening a publication](open-publication.md)
* [Extracting the content of a publication](content.md)
* [Supporting PDF documents](pdf.md)
* [Configuring the Navigator](navigator-preferences.md)
Expand Down
69 changes: 69 additions & 0 deletions docs/guides/open-publication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Opening a publication

:warning: The described components are is still experimental.

Readium requires you to instantiate a few components before you can actually open a publication.

## Constructing an `AssetOpener`

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.

```kotlin
val httpClient = DefaultHttpClient()

val assetOpener = AssetOpener(context.contentResolver, httpClient)
```

## Constructing a `PublicationOpener`

The component which can parse an `Asset` giving access to a publication to build a proper `Publication`
object is the `PublicationOpener`. Its constructor requires you to pass in:

* a `PublicationParser` it delegates the parsing work to.
* a list of `ContentProtection`s which will deal with DRMs.

The easiest way to get a `PublicationParser` is to use the `DefaultPublicationParser` class. As to
`ContentProtection`s, you can get one to support LCP publications through your `LcpService` if you have one.

```kotlin
val contentProtections = listOf(lcpService.contentProtection(authentication))

val publicationParser = DefaultPublicationParser(context, httpClient, assetOpener, 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:
```kotlin
val asset = assetOpener.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
for the content format, especially in case of remote publications.

## Extensibility`

`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.
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.

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 @@ -56,7 +56,7 @@ public fun Error.toDebugDescription(): String =
} else {
var desc = "${javaClass.nameWithEnclosingClasses()}: $message"
cause?.let { cause ->
desc += "\n\n${cause.toDebugDescription()}"
desc += "\n${cause.toDebugDescription()}"
}
desc
}
Expand All @@ -67,7 +67,7 @@ private fun Throwable.toDebugDescription(): String {
desc += message ?: ""
desc += "\n" + stackTrace.take(2).joinToString("\n").prependIndent(" ")
cause?.let { cause ->
desc += "\n\n${cause.toDebugDescription()}"
desc += "\n${cause.toDebugDescription()}"
}
return desc
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,22 @@

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.file.FileResourceFactory
import org.readium.r2.shared.util.format.DefaultFormatSniffer
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
import org.readium.r2.shared.util.zip.ZipArchiveOpener

/**
* Retrieves an [Asset] instance providing reading access to the resource(s) of an asset stored at
Expand All @@ -34,11 +33,20 @@ public class AssetOpener(
private val archiveOpener: ArchiveOpener
) {
public constructor(
resourceFactory: ResourceFactory = FileResourceFactory(),
archiveOpener: ArchiveOpener = ZipArchiveOpener(),
formatSniffer: FormatSniffer = DefaultFormatSniffer()
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?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ 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.Format
import org.readium.r2.shared.util.format.FormatHints
import org.readium.r2.shared.util.format.FormatSniffer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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.content.ContentResourceFactory
import org.readium.r2.shared.util.file.FileResourceFactory
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.CompositeFormatSniffer
import org.readium.r2.shared.util.format.EpubDrmSniffer
import org.readium.r2.shared.util.format.EpubSniffer
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.LpfSniffer
import org.readium.r2.shared.util.format.Opds1Sniffer
import org.readium.r2.shared.util.format.Opds2Sniffer
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.http.HttpClient
import org.readium.r2.shared.util.http.HttpResourceFactory
import org.readium.r2.shared.util.resource.CompositeResourceFactory
import org.readium.r2.shared.util.resource.ResourceFactory
import org.readium.r2.shared.util.zip.ZipArchiveOpener

/**
* Default implementation of [ResourceFactory] supporting file, content and http schemes.
*
* @param contentResolver content resolver to use to support content scheme.
* @param httpClient Http client to use to support http scheme.
* @param additionalFactories Additional [ResourceFactory] to support additional schemes.
*/
public class DefaultResourceFactory(
contentResolver: ContentResolver,
httpClient: HttpClient,
additionalFactories: List<ResourceFactory> = emptyList()
) : ResourceFactory by CompositeResourceFactory(
*additionalFactories.toTypedArray(),
FileResourceFactory(),
ContentResourceFactory(contentResolver),
HttpResourceFactory(httpClient)
)

/**
* Default implementation of [ArchiveOpener] supporting only ZIP archives.
*
* @param additionalOpeners Additional openers to be used.
*/
public class DefaultArchiveOpener(
additionalOpeners: List<ArchiveOpener> = emptyList()
) : ArchiveOpener by CompositeArchiveOpener(
*additionalOpeners.toTypedArray(),
ZipArchiveOpener()
)

/**
* Default implementation of [FormatSniffer] guessing as well as possible all formats known by
* Readium.
*
* @param additionalSniffers Additional sniffers to be used to guess content format.
*/
public class DefaultFormatSniffer(
additionalSniffers: List<FormatSniffer> = emptyList()
) : FormatSniffer by CompositeFormatSniffer(
*additionalSniffers.toTypedArray(),
ZipSniffer,
RarSniffer,
EpubSniffer,
LpfSniffer,
ArchiveSniffer,
RpfSniffer,
PdfSniffer,
HtmlSniffer,
BitmapSniffer,
AudioSniffer,
JsonSniffer,
Opds1Sniffer,
Opds2Sniffer,
LcpLicenseSniffer,
EpubDrmSniffer,
W3cWpubSniffer,
RwpmSniffer
)
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,6 @@ import org.readium.r2.shared.util.getOrDefault
import org.readium.r2.shared.util.getOrElse
import org.readium.r2.shared.util.mediatype.MediaType

public class DefaultFormatSniffer :
FormatSniffer by CompositeFormatSniffer(
ZipSniffer,
RarSniffer,
EpubSniffer,
LpfSniffer,
ArchiveSniffer,
RpfSniffer,
PdfSniffer,
HtmlSniffer,
BitmapSniffer,
AudioSniffer,
JsonSniffer,
Opds1Sniffer,
Opds2Sniffer,
LcpLicenseSniffer,
EpubDrmSniffer,
W3cWpubSniffer,
RwpmSniffer
)

/** Sniffs an HTML or XHTML document. */
public object HtmlSniffer : FormatSniffer {
override fun sniffHints(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import java.net.NoRouteToHostException
import java.net.SocketTimeoutException
import java.net.URL
import java.net.UnknownHostException
import javax.net.ssl.SSLHandshakeException
import kotlin.time.Duration
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -327,6 +328,8 @@ private fun wrap(cause: IOException): HttpError =
HttpError.Unreachable(ThrowableError(cause))
is SocketTimeoutException ->
HttpError.Timeout(ThrowableError(cause))
is SSLHandshakeException ->
HttpError.SslHandshake(ThrowableError(cause))
else ->
HttpError.IO(cause)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,26 @@ public sealed class HttpError(
public override val cause: Error? = null
) : AccessError {

/** Malformed HTTP response. */
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)

/** Server could not be reached. */
public class Unreachable(cause: Error) :
HttpError("Server could not be reached.", cause)

/** Redirection failed. */
public class Redirection(cause: Error) :
HttpError("Redirection failed.", cause)

/** SSL Handshake failed. */
public class SslHandshake(cause: Error) :
HttpError("SSL handshake failed.", cause)

/** An unknown networking error. */
public class IO(cause: Error) :
HttpError("An IO error occurred.", cause) {
Expand All @@ -42,6 +49,8 @@ public sealed class HttpError(
}

/**
* Server responded with an error status code.
*
* @param status HTTP status code.
* @param mediaType Response media type.
* @param body Response body.
Expand Down

This file was deleted.

Loading