Skip to content

Commit d8c3cd2

Browse files
authored
Relax rules when parsing a RWPM (#412)
1 parent 7b0fb79 commit d8c3cd2

File tree

18 files changed

+171
-89
lines changed

18 files changed

+171
-89
lines changed

readium/lcp/src/main/java/org/readium/r2/lcp/license/model/components/Link.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ public data class Link(
3535
json: JSONObject,
3636
mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever()
3737
): Link {
38-
val hrefString = json.optNullableString("href")
38+
val href = json.optNullableString("href")
39+
?.let {
40+
Href(
41+
href = it,
42+
templated = json.optBoolean("templated", false)
43+
)
44+
}
3945
?: throw LcpException.Parsing.Link
40-
val href = Href(
41-
href = hrefString,
42-
templated = json.optBoolean("templated", false)
43-
) ?: throw LcpException.Parsing.Link
4446

4547
return Link(
4648
href = href,

readium/shared/src/main/java/org/readium/r2/shared/publication/Href.kt

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,23 @@ import timber.log.Timber
2121
@Parcelize
2222
public class Href private constructor(private val href: Url) : Parcelable {
2323

24+
/**
25+
* Creates an [Href] from a valid URL.
26+
*/
27+
public constructor(href: SharedUrl) : this(StaticUrl(href))
28+
2429
public companion object {
25-
public operator fun invoke(href: SharedUrl): Href =
26-
Href(StaticUrl(href))
27-
28-
public operator fun invoke(href: String, templated: Boolean = false): Href? {
29-
val url =
30-
if (templated) {
31-
TemplatedUrl(href)
32-
} else {
33-
SharedUrl(href)?.let { StaticUrl(it) }
34-
}
35-
36-
return url?.let { Href(it) }
37-
}
30+
/**
31+
* Creates an [Href] from a valid URL or URL template (RFC 6570).
32+
*
33+
* @param templated Indicates whether [href] is a URL template.
34+
*/
35+
public operator fun invoke(href: String, templated: Boolean = false): Href? =
36+
if (templated) {
37+
Href(TemplatedUrl(href))
38+
} else {
39+
SharedUrl(href)?.let { Href(StaticUrl(it)) }
40+
}
3841
}
3942

4043
/**

readium/shared/src/main/java/org/readium/r2/shared/publication/Link.kt

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -165,21 +165,10 @@ public data class Link(
165165
mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(),
166166
warnings: WarningLogger? = null
167167
): Link? {
168-
val hrefString = json?.optNullableString("href")
169-
if (hrefString == null) {
170-
warnings?.log(Link::class.java, "[href] is required", json)
171-
return null
172-
}
173-
val href = Href(
174-
href = hrefString,
175-
templated = json.optBoolean("templated", false)
176-
) ?: run {
177-
warnings?.log(Link::class.java, "[href] is not a valid URL or URL template", json)
178-
return null
179-
}
168+
json ?: return null
180169

181170
return Link(
182-
href = href,
171+
href = parseHref(json, warnings) ?: return null,
183172
mediaType = json.optNullableString("type")
184173
?.let { mediaTypeRetriever.retrieve(it) },
185174
title = json.optNullableString("title"),
@@ -201,6 +190,41 @@ public data class Link(
201190
)
202191
}
203192

193+
private fun parseHref(
194+
json: JSONObject,
195+
warnings: WarningLogger? = null
196+
): Href? {
197+
val hrefString = json.optNullableString("href")
198+
if (hrefString == null) {
199+
warnings?.log(Link::class.java, "[href] is required", json)
200+
return null
201+
}
202+
203+
val templated = json.optBoolean("templated", false)
204+
val href = if (templated) {
205+
Href(hrefString, templated = true)
206+
} else {
207+
// We support existing publications with incorrect HREFs (not valid percent-encoded
208+
// URIs). We try to parse them first as valid, but fall back on a percent-decoded
209+
// path if it fails.
210+
val url = Url(hrefString) ?: run {
211+
warnings?.log(
212+
Link::class.java,
213+
"[href] is not a valid percent-encoded URL",
214+
json
215+
)
216+
Url.fromDecodedPath(hrefString)
217+
}
218+
url?.let { Href(it) }
219+
}
220+
221+
if (href == null) {
222+
warnings?.log(Link::class.java, "[href] is not a valid URL or URL template", json)
223+
}
224+
225+
return href
226+
}
227+
204228
/**
205229
* Creates a list of [Link] from its RWPM JSON representation.
206230
*

readium/shared/src/main/java/org/readium/r2/shared/publication/Manifest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.readium.r2.shared.extensions.optStringsFromArrayOrSingle
1616
import org.readium.r2.shared.extensions.putIfNotEmpty
1717
import org.readium.r2.shared.toJSON
1818
import org.readium.r2.shared.util.Url
19+
import org.readium.r2.shared.util.logging.ConsoleWarningLogger
1920
import org.readium.r2.shared.util.logging.WarningLogger
2021
import org.readium.r2.shared.util.logging.log
2122
import org.readium.r2.shared.util.mediatype.MediaType
@@ -154,7 +155,7 @@ public data class Manifest(
154155
public fun fromJSON(
155156
json: JSONObject?,
156157
mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever(),
157-
warnings: WarningLogger? = null
158+
warnings: WarningLogger? = ConsoleWarningLogger()
158159
): Manifest? {
159160
json ?: return null
160161

readium/shared/src/main/java/org/readium/r2/shared/util/logging/WarningLogger.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public data class JsonWarning(
109109

110110
override val tag: String = "json"
111111

112-
override val message: String get() = "${javaClass.name} ${modelClass.name}: $reason"
112+
override val message: String get() = "${javaClass.simpleName} ${modelClass.name}: $reason"
113113
}
114114

115115
/**

readium/streamer/src/main/java/org/readium/r2/streamer/ParserAssetFactory.kt

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import org.readium.r2.shared.extensions.addPrefix
1212
import org.readium.r2.shared.publication.Manifest
1313
import org.readium.r2.shared.publication.Publication
1414
import org.readium.r2.shared.util.AbsoluteUrl
15-
import org.readium.r2.shared.util.MessageError
1615
import org.readium.r2.shared.util.ThrowableError
1716
import org.readium.r2.shared.util.Try
1817
import org.readium.r2.shared.util.Url
@@ -28,6 +27,7 @@ import org.readium.r2.shared.util.resource.Resource
2827
import org.readium.r2.shared.util.resource.ResourceContainer
2928
import org.readium.r2.shared.util.resource.RoutingContainer
3029
import org.readium.r2.streamer.parser.PublicationParser
30+
import timber.log.Timber
3131

3232
internal class ParserAssetFactory(
3333
private val httpClient: HttpClient,
@@ -36,18 +36,18 @@ internal class ParserAssetFactory(
3636
) {
3737

3838
suspend fun createParserAsset(
39-
asset: org.readium.r2.shared.util.asset.Asset
39+
asset: Asset
4040
): Try<PublicationParser.Asset, Publication.OpenError> {
4141
return when (asset) {
42-
is org.readium.r2.shared.util.asset.Asset.Container ->
42+
is Asset.Container ->
4343
createParserAssetForContainer(asset)
44-
is org.readium.r2.shared.util.asset.Asset.Resource ->
44+
is Asset.Resource ->
4545
createParserAssetForResource(asset)
4646
}
4747
}
4848

4949
private fun createParserAssetForContainer(
50-
asset: org.readium.r2.shared.util.asset.Asset.Container
50+
asset: Asset.Container
5151
): Try<PublicationParser.Asset, Publication.OpenError> =
5252
Try.success(
5353
PublicationParser.Asset(
@@ -57,7 +57,7 @@ internal class ParserAssetFactory(
5757
)
5858

5959
private suspend fun createParserAssetForResource(
60-
asset: org.readium.r2.shared.util.asset.Asset.Resource
60+
asset: Asset.Resource
6161
): Try<PublicationParser.Asset, Publication.OpenError> =
6262
if (asset.mediaType.isRwpm) {
6363
createParserAssetForManifest(asset)
@@ -66,34 +66,28 @@ internal class ParserAssetFactory(
6666
}
6767

6868
private suspend fun createParserAssetForManifest(
69-
asset: org.readium.r2.shared.util.asset.Asset.Resource
69+
asset: Asset.Resource
7070
): Try<PublicationParser.Asset, Publication.OpenError> {
7171
val manifest = asset.resource.readAsRwpm()
7272
.mapFailure { Publication.OpenError.InvalidAsset(ThrowableError(it)) }
7373
.getOrElse { return Try.failure(it) }
7474

75-
val baseUrl =
76-
manifest.linkWithRel("self")?.href?.resolve()
77-
?: return Try.failure(
78-
Publication.OpenError.InvalidAsset(
79-
MessageError("No self link in the manifest.")
80-
)
81-
)
82-
83-
if (baseUrl !is AbsoluteUrl) {
84-
return Try.failure(
85-
Publication.OpenError.InvalidAsset(
86-
MessageError("Self link is not absolute.")
75+
val baseUrl = manifest.linkWithRel("self")?.href?.resolve()
76+
if (baseUrl == null) {
77+
Timber.w("No self link found in the manifest at ${asset.resource.source}")
78+
} else {
79+
if (baseUrl !is AbsoluteUrl) {
80+
return Try.failure(
81+
Publication.OpenError.InvalidAsset("Self link is not absolute.")
8782
)
88-
)
89-
}
90-
91-
if (!baseUrl.isHttp) {
92-
return Try.failure(
93-
Publication.OpenError.UnsupportedAsset(
94-
"Self link doesn't use the HTTP(S) scheme."
83+
}
84+
if (!baseUrl.isHttp) {
85+
return Try.failure(
86+
Publication.OpenError.UnsupportedAsset(
87+
"Self link doesn't use the HTTP(S) scheme."
88+
)
9589
)
96-
)
90+
}
9791
}
9892

9993
val container =
@@ -114,7 +108,7 @@ internal class ParserAssetFactory(
114108
}
115109

116110
private fun createParserAssetForContent(
117-
asset: org.readium.r2.shared.util.asset.Asset.Resource
111+
asset: Asset.Resource
118112
): Try<PublicationParser.Asset, Publication.OpenError> {
119113
// Historically, the reading order of a standalone file contained a single link with the
120114
// HREF "/$assetName". This was fragile if the asset named changed, or was different on

test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import androidx.navigation.ui.setupActionBarWithNavController
1616
import androidx.navigation.ui.setupWithNavController
1717
import com.google.android.material.bottomnavigation.BottomNavigationView
1818
import com.google.android.material.snackbar.Snackbar
19+
import org.readium.r2.testapp.utils.extensions.readium.toDebugDescription
20+
import timber.log.Timber
1921

2022
class MainActivity : AppCompatActivity() {
2123

@@ -59,7 +61,8 @@ class MainActivity : AppCompatActivity() {
5961
getString(R.string.import_publication_success)
6062

6163
is MainViewModel.Event.ImportPublicationError -> {
62-
event.errorMessage
64+
Timber.e(event.error.toDebugDescription(this))
65+
event.error.getUserMessage(this)
6366
}
6467
}
6568
Snackbar.make(

test-app/src/main/java/org/readium/r2/testapp/MainViewModel.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.onEach
1616
import kotlinx.coroutines.flow.receiveAsFlow
1717
import kotlinx.coroutines.launch
1818
import org.readium.r2.testapp.domain.Bookshelf
19+
import org.readium.r2.testapp.domain.ImportError
1920
import org.readium.r2.testapp.utils.EventChannel
2021

2122
class MainViewModel(
@@ -41,8 +42,7 @@ class MainViewModel(
4142
private fun sendImportFeedback(event: Bookshelf.Event) {
4243
when (event) {
4344
is Bookshelf.Event.ImportPublicationError -> {
44-
val errorMessage = event.error.getUserMessage(app)
45-
channel.send(Event.ImportPublicationError(errorMessage))
45+
channel.send(Event.ImportPublicationError(event.error))
4646
}
4747
Bookshelf.Event.ImportPublicationSuccess -> {
4848
channel.send(Event.ImportPublicationSuccess)
@@ -56,7 +56,7 @@ class MainViewModel(
5656
Event()
5757

5858
class ImportPublicationError(
59-
val errorMessage: String
59+
val error: ImportError
6060
) : Event()
6161
}
6262
}

test-app/src/main/java/org/readium/r2/testapp/data/BookRepository.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import org.readium.r2.testapp.data.db.BooksDao
2121
import org.readium.r2.testapp.data.model.Book
2222
import org.readium.r2.testapp.data.model.Bookmark
2323
import org.readium.r2.testapp.data.model.Highlight
24-
import org.readium.r2.testapp.utils.extensions.authorName
24+
import org.readium.r2.testapp.utils.extensions.readium.authorName
2525

2626
class BookRepository(
2727
private val booksDao: BooksDao

test-app/src/main/java/org/readium/r2/testapp/outline/BookmarksFragment.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import org.readium.r2.testapp.data.model.Bookmark
2828
import org.readium.r2.testapp.databinding.FragmentListviewBinding
2929
import org.readium.r2.testapp.databinding.ItemRecycleBookmarkBinding
3030
import org.readium.r2.testapp.reader.ReaderViewModel
31-
import org.readium.r2.testapp.utils.extensions.outlineTitle
31+
import org.readium.r2.testapp.utils.extensions.readium.outlineTitle
3232
import org.readium.r2.testapp.utils.viewLifecycle
3333

3434
class BookmarksFragment : Fragment() {

0 commit comments

Comments
 (0)