Skip to content

Commit 9fe0d30

Browse files
authored
Refactor Url and Href (#388)
1 parent 35f00bb commit 9fe0d30

File tree

232 files changed

+6511
-3588
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

232 files changed

+6511
-3588
lines changed

readium/adapters/pdfium/pdfium-document/src/main/java/org/readium/adapters/pdfium/document/PdfiumDocument.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import org.readium.r2.shared.resource.Resource
2222
import org.readium.r2.shared.util.getOrThrow
2323
import org.readium.r2.shared.util.pdf.PdfDocument
2424
import org.readium.r2.shared.util.pdf.PdfDocumentFactory
25-
import org.readium.r2.shared.util.toFile
2625
import org.readium.r2.shared.util.use
2726
import timber.log.Timber
2827

readium/lcp/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@ dependencies {
6767
exclude(module = "support-v4")
6868
}
6969
implementation(libs.joda.time)
70-
implementation("org.zeroturnaround:zt-zip:1.15")
7170
implementation(libs.androidx.browser)
7271

7372
implementation(libs.bundles.room)
7473
ksp(libs.androidx.room.compiler)
7574

7675
// Tests
7776
testImplementation(libs.junit)
77+
testImplementation(libs.kotlin.junit)
7878

7979
androidTestImplementation(libs.androidx.ext.junit)
8080
androidTestImplementation(libs.androidx.expresso.core)

readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt

Lines changed: 55 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,13 @@ import org.readium.r2.shared.publication.encryption.encryption
1616
import org.readium.r2.shared.publication.flatten
1717
import org.readium.r2.shared.publication.protection.ContentProtection
1818
import org.readium.r2.shared.publication.services.contentProtectionServiceFactory
19-
import org.readium.r2.shared.resource.ArchiveFactory
2019
import org.readium.r2.shared.resource.Resource
21-
import org.readium.r2.shared.resource.ResourceFactory
2220
import org.readium.r2.shared.resource.TransformingContainer
21+
import org.readium.r2.shared.util.AbsoluteUrl
2322
import org.readium.r2.shared.util.ThrowableError
2423
import org.readium.r2.shared.util.Try
25-
import org.readium.r2.shared.util.Url
2624
import org.readium.r2.shared.util.flatMap
2725
import org.readium.r2.shared.util.getOrElse
28-
import org.readium.r2.shared.util.toFile
2926

3027
internal class LcpContentProtection(
3128
private val lcpService: LcpService,
@@ -46,7 +43,7 @@ internal class LcpContentProtection(
4643
credentials: String?,
4744
allowUserInteraction: Boolean,
4845
sender: Any?
49-
): Try<ContentProtection.Asset, Publication.OpeningException> {
46+
): Try<ContentProtection.Asset, Publication.OpenError> {
5047
return when (asset) {
5148
is Asset.Container -> openPublication(asset, credentials, allowUserInteraction, sender)
5249
is Asset.Resource -> openLicense(asset, credentials, allowUserInteraction, sender)
@@ -58,7 +55,7 @@ internal class LcpContentProtection(
5855
credentials: String?,
5956
allowUserInteraction: Boolean,
6057
sender: Any?
61-
): Try<ContentProtection.Asset, Publication.OpeningException> {
58+
): Try<ContentProtection.Asset, Publication.OpenError> {
6259
val license = retrieveLicense(asset, credentials, allowUserInteraction, sender)
6360
return createResultAsset(asset, license)
6461
}
@@ -73,27 +70,13 @@ internal class LcpContentProtection(
7370
?.let { LcpPassphraseAuthentication(it, fallback = this.authentication) }
7471
?: this.authentication
7572

76-
val file = (asset as? Asset.Resource)?.resource?.source?.toFile()
77-
?: (asset as? Asset.Container)?.container?.source?.toFile()
78-
79-
return file
80-
// This is less restrictive with regard to network availability.
81-
?.let {
82-
lcpService.retrieveLicense(
83-
it,
84-
asset.mediaType,
85-
authentication,
86-
allowUserInteraction,
87-
sender
88-
)
89-
}
90-
?: lcpService.retrieveLicense(asset, authentication, allowUserInteraction, sender)
73+
return lcpService.retrieveLicense(asset, authentication, allowUserInteraction, sender)
9174
}
9275

9376
private fun createResultAsset(
9477
asset: Asset.Container,
9578
license: Try<LcpLicense, LcpException>
96-
): Try<ContentProtection.Asset, Publication.OpeningException> {
79+
): Try<ContentProtection.Asset, Publication.OpenError> {
9780
val serviceFactory = LcpContentProtectionService
9881
.createFactory(license.getOrNull(), license.failureOrNull())
9982

@@ -107,7 +90,9 @@ internal class LcpContentProtection(
10790
onCreatePublication = {
10891
decryptor.encryptionData = (manifest.readingOrder + manifest.resources + manifest.links)
10992
.flatten()
110-
.mapNotNull { it.properties.encryption?.let { enc -> it.href to enc } }
93+
.mapNotNull {
94+
it.properties.encryption?.let { enc -> it.url() to enc }
95+
}
11196
.toMap()
11297

11398
servicesBuilder.contentProtectionServiceFactory = serviceFactory
@@ -122,7 +107,7 @@ internal class LcpContentProtection(
122107
credentials: String?,
123108
allowUserInteraction: Boolean,
124109
sender: Any?
125-
): Try<ContentProtection.Asset, Publication.OpeningException> {
110+
): Try<ContentProtection.Asset, Publication.OpenError> {
126111
val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction, sender)
127112

128113
val licenseDoc = license.getOrNull()?.license
@@ -132,9 +117,7 @@ internal class LcpContentProtection(
132117
LicenseDocument(it)
133118
} catch (e: Exception) {
134119
return Try.failure(
135-
Publication.OpeningException.ParsingFailed(
136-
ThrowableError(e)
137-
)
120+
Publication.OpenError.InvalidAsset(cause = ThrowableError(e))
138121
)
139122
}
140123
}
@@ -145,55 +128,64 @@ internal class LcpContentProtection(
145128
}
146129

147130
val link = checkNotNull(licenseDoc.link(LicenseDocument.Rel.Publication))
148-
val url = Url(link.url.toString())
131+
val url = (link.url() as? AbsoluteUrl)
149132
?: return Try.failure(
150-
Publication.OpeningException.ParsingFailed(
151-
ThrowableError(
133+
Publication.OpenError.InvalidAsset(
134+
cause = ThrowableError(
152135
LcpException.Parsing.Url(rel = LicenseDocument.Rel.Publication.value)
153136
)
154137
)
155138
)
156139

157-
return assetRetriever.retrieve(
158-
url,
159-
mediaType = link.mediaType,
160-
assetType = AssetType.Archive
161-
)
162-
.mapFailure { Publication.OpeningException.ParsingFailed(it) }
163-
.flatMap { createResultAsset(it as Asset.Container, license) }
164-
}
165-
166-
private fun ResourceFactory.Error.wrap(): Publication.OpeningException =
167-
when (this) {
168-
is ResourceFactory.Error.NotAResource ->
169-
Publication.OpeningException.NotFound()
170-
is ResourceFactory.Error.Forbidden ->
171-
Publication.OpeningException.Forbidden()
172-
is ResourceFactory.Error.SchemeNotSupported ->
173-
Publication.OpeningException.UnsupportedAsset()
174-
}
140+
val asset =
141+
if (link.mediaType != null) {
142+
assetRetriever.retrieve(
143+
url,
144+
mediaType = link.mediaType,
145+
assetType = AssetType.Archive
146+
)
147+
.map { it as Asset.Container }
148+
.mapFailure { it.wrap() }
149+
} else {
150+
(assetRetriever.retrieve(url) as? Asset.Container)
151+
?.let { Try.success(it) }
152+
?: Try.failure(Publication.OpenError.UnsupportedAsset())
153+
}
175154

176-
private fun ArchiveFactory.Error.wrap(): Publication.OpeningException =
177-
when (this) {
178-
is ArchiveFactory.Error.FormatNotSupported ->
179-
Publication.OpeningException.UnsupportedAsset()
180-
is ArchiveFactory.Error.PasswordsNotSupported ->
181-
Publication.OpeningException.UnsupportedAsset()
182-
is ArchiveFactory.Error.ResourceReading ->
183-
resourceException.wrap()
184-
}
155+
return asset.flatMap { createResultAsset(it, license) }
156+
}
185157

186-
private fun Resource.Exception.wrap(): Publication.OpeningException =
158+
private fun Resource.Exception.wrap(): Publication.OpenError =
187159
when (this) {
188160
is Resource.Exception.Forbidden ->
189-
Publication.OpeningException.Forbidden(ThrowableError(this))
161+
Publication.OpenError.Forbidden(ThrowableError(this))
190162
is Resource.Exception.NotFound ->
191-
Publication.OpeningException.NotFound(ThrowableError(this))
163+
Publication.OpenError.NotFound(ThrowableError(this))
192164
Resource.Exception.Offline, is Resource.Exception.Unavailable ->
193-
Publication.OpeningException.Unavailable(ThrowableError(this))
165+
Publication.OpenError.Unavailable(ThrowableError(this))
194166
is Resource.Exception.Other, is Resource.Exception.BadRequest ->
195-
Publication.OpeningException.Unexpected(this)
167+
Publication.OpenError.Unknown(this)
196168
is Resource.Exception.OutOfMemory ->
197-
Publication.OpeningException.OutOfMemory(ThrowableError(this))
169+
Publication.OpenError.OutOfMemory(ThrowableError(this))
170+
}
171+
172+
private fun AssetRetriever.Error.wrap(): Publication.OpenError =
173+
when (this) {
174+
is AssetRetriever.Error.ArchiveFormatNotSupported ->
175+
Publication.OpenError.UnsupportedAsset(this)
176+
is AssetRetriever.Error.Forbidden ->
177+
Publication.OpenError.Forbidden(this)
178+
is AssetRetriever.Error.InvalidAsset ->
179+
Publication.OpenError.InvalidAsset(this)
180+
is AssetRetriever.Error.NotFound ->
181+
Publication.OpenError.NotFound(this)
182+
is AssetRetriever.Error.OutOfMemory ->
183+
Publication.OpenError.OutOfMemory(this)
184+
is AssetRetriever.Error.SchemeNotSupported ->
185+
Publication.OpenError.UnsupportedAsset(this)
186+
is AssetRetriever.Error.Unavailable ->
187+
Publication.OpenError.Unavailable(this)
188+
is AssetRetriever.Error.Unknown ->
189+
Publication.OpenError.Unknown(this)
198190
}
199191
}

readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import org.readium.r2.shared.resource.TransformingResource
2222
import org.readium.r2.shared.resource.flatMap
2323
import org.readium.r2.shared.resource.flatMapCatching
2424
import org.readium.r2.shared.resource.mapCatching
25+
import org.readium.r2.shared.util.AbsoluteUrl
2526
import org.readium.r2.shared.util.Try
2627
import org.readium.r2.shared.util.Url
2728
import org.readium.r2.shared.util.getOrElse
@@ -32,7 +33,7 @@ import org.readium.r2.shared.util.getOrThrow
3233
*/
3334
internal class LcpDecryptor(
3435
val license: LcpLicense?,
35-
var encryptionData: Map<String, Encryption> = emptyMap()
36+
var encryptionData: Map<Url, Encryption> = emptyMap()
3637
) {
3738

3839
fun transform(resource: Resource): Resource {
@@ -41,7 +42,7 @@ internal class LcpDecryptor(
4142
}
4243

4344
return resource.flatMap {
44-
val encryption = encryptionData[resource.path]
45+
val encryption = encryptionData[resource.url]
4546

4647
// Checks if the resource is encrypted and whether the encryption schemes of the resource
4748
// and the DRM license are the same.
@@ -93,7 +94,7 @@ internal class LcpDecryptor(
9394
private val license: LcpLicense
9495
) : Resource by resource {
9596

96-
override val source: Url? = null
97+
override val source: AbsoluteUrl? = null
9798

9899
private class Cache(
99100
var startIndex: Int? = null,
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2023 Readium Foundation. All rights reserved.
3+
* Use of this source code is governed by the BSD-style license
4+
* available in the top-level LICENSE file of the project.
5+
*/
6+
7+
package org.readium.r2.lcp
8+
9+
import android.content.Context
10+
import java.io.File
11+
import java.util.LinkedList
12+
import kotlinx.coroutines.Deferred
13+
import kotlinx.coroutines.Dispatchers
14+
import kotlinx.coroutines.async
15+
import kotlinx.coroutines.launch
16+
import kotlinx.coroutines.withContext
17+
import org.json.JSONObject
18+
import org.readium.r2.shared.util.CoroutineQueue
19+
20+
internal class LcpDownloadsRepository(
21+
context: Context
22+
) {
23+
private val queue = CoroutineQueue()
24+
25+
private val storageDir: Deferred<File> =
26+
queue.scope.async {
27+
withContext(Dispatchers.IO) {
28+
File(context.noBackupFilesDir, LcpDownloadsRepository::class.qualifiedName!!)
29+
.also { if (!it.exists()) it.mkdirs() }
30+
}
31+
}
32+
33+
private val storageFile: Deferred<File> =
34+
queue.scope.async {
35+
withContext(Dispatchers.IO) {
36+
File(storageDir.await(), "licenses.json")
37+
.also { if (!it.exists()) { it.writeText("{}", Charsets.UTF_8) } }
38+
}
39+
}
40+
41+
private val snapshot: Deferred<MutableMap<String, JSONObject>> =
42+
queue.scope.async {
43+
readSnapshot().toMutableMap()
44+
}
45+
46+
fun addDownload(id: String, license: JSONObject) {
47+
queue.scope.launch {
48+
val snapshotCompleted = snapshot.await()
49+
snapshotCompleted[id] = license
50+
writeSnapshot(snapshotCompleted)
51+
}
52+
}
53+
54+
fun removeDownload(id: String) {
55+
queue.launch {
56+
val snapshotCompleted = snapshot.await()
57+
snapshotCompleted.remove(id)
58+
writeSnapshot(snapshotCompleted)
59+
}
60+
}
61+
62+
suspend fun retrieveLicense(id: String): JSONObject? =
63+
queue.await {
64+
snapshot.await()[id]
65+
}
66+
67+
private suspend fun readSnapshot(): Map<String, JSONObject> {
68+
return withContext(Dispatchers.IO) {
69+
storageFile.await().readText(Charsets.UTF_8).toData().toMutableMap()
70+
}
71+
}
72+
73+
private suspend fun writeSnapshot(snapshot: Map<String, JSONObject>) {
74+
val storageFileCompleted = storageFile.await()
75+
withContext(Dispatchers.IO) {
76+
storageFileCompleted.writeText(snapshot.toJson(), Charsets.UTF_8)
77+
}
78+
}
79+
80+
private fun Map<String, JSONObject>.toJson(): String {
81+
val jsonObject = JSONObject()
82+
for ((id, license) in this.entries) {
83+
jsonObject.put(id, license)
84+
}
85+
return jsonObject.toString()
86+
}
87+
88+
private fun String.toData(): Map<String, JSONObject> {
89+
val jsonObject = JSONObject(this)
90+
val names = jsonObject.keys().iterator().toList()
91+
return names.associateWith { jsonObject.getJSONObject(it) }
92+
}
93+
94+
private fun <T> Iterator<T>.toList(): List<T> =
95+
LinkedList<T>().apply {
96+
while (hasNext())
97+
this += next()
98+
}.toMutableList()
99+
}

readium/lcp/src/main/java/org/readium/r2/lcp/LcpException.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.annotation.StringRes
1111
import java.net.SocketTimeoutException
1212
import java.util.*
1313
import org.readium.r2.shared.UserException
14+
import org.readium.r2.shared.util.Url
1415

1516
public sealed class LcpException(
1617
userMessageId: Int,
@@ -203,17 +204,17 @@ public sealed class LcpException(
203204
public object OpenFailed : Container(R.string.readium_lcp_exception_container_open_failed)
204205

205206
/** The file at given relative path is not found in the Container. */
206-
public class FileNotFound(public val path: String) : Container(
207+
public class FileNotFound(public val url: Url) : Container(
207208
R.string.readium_lcp_exception_container_file_not_found
208209
)
209210

210211
/** Can't read the file at given relative path in the Container. */
211-
public class ReadFailed(public val path: String) : Container(
212+
public class ReadFailed(public val url: Url?) : Container(
212213
R.string.readium_lcp_exception_container_read_failed
213214
)
214215

215216
/** Can't write the file at given relative path in the Container. */
216-
public class WriteFailed(public val path: String) : Container(
217+
public class WriteFailed(public val url: Url?) : Container(
217218
R.string.readium_lcp_exception_container_write_failed
218219
)
219220
}

0 commit comments

Comments
 (0)