From 7fc51f358409749a2e38f04a221e565ec9efff76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Fri, 7 Feb 2020 18:58:02 +0100 Subject: [PATCH 1/8] Refactor the Locator models --- .../java/org/readium/r2/shared/Deprecated.kt | 14 + .../java/org/readium/r2/shared/Locator.kt | 285 ------------------ .../org/readium/r2/shared/extensions/JSON.kt | 4 +- .../readium/r2/shared/publication/Locator.kt | 198 ++++++++++++ .../r2/shared/publication/html/DomRange.kt | 122 ++++++++ .../r2/shared/publication/html/Locator.kt | 39 +++ .../r2/shared/publication/LocatorTest.kt | 264 ++++++++++++++++ .../shared/publication/html/DomRangeTest.kt | 190 ++++++++++++ .../r2/shared/publication/html/LocatorTest.kt | 58 ++++ 9 files changed, 887 insertions(+), 287 deletions(-) delete mode 100644 r2-shared/src/main/java/org/readium/r2/shared/Locator.kt create mode 100644 r2-shared/src/main/java/org/readium/r2/shared/publication/Locator.kt create mode 100644 r2-shared/src/main/java/org/readium/r2/shared/publication/html/DomRange.kt create mode 100644 r2-shared/src/main/java/org/readium/r2/shared/publication/html/Locator.kt create mode 100644 r2-shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt create mode 100644 r2-shared/src/test/java/org/readium/r2/shared/publication/html/DomRangeTest.kt create mode 100644 r2-shared/src/test/java/org/readium/r2/shared/publication/html/LocatorTest.kt diff --git a/r2-shared/src/main/java/org/readium/r2/shared/Deprecated.kt b/r2-shared/src/main/java/org/readium/r2/shared/Deprecated.kt index c0a5103a..12859121 100644 --- a/r2-shared/src/main/java/org/readium/r2/shared/Deprecated.kt +++ b/r2-shared/src/main/java/org/readium/r2/shared/Deprecated.kt @@ -23,6 +23,20 @@ import org.readium.r2.shared.publication.encryption.Encryption import org.readium.r2.shared.publication.presentation.Presentation import java.net.URL +@Deprecated("Moved to another package", ReplaceWith("org.readium.r2.shared.publication.Locator")) +typealias Locator = org.readium.r2.shared.publication.Locator + +@Deprecated("Renamed into [Locator.Locations]", ReplaceWith("Locator.Locations", "org.readium.r2.shared.publication.Locator")) +typealias Locations = org.readium.r2.shared.publication.Locator.Locations + +@Deprecated("Renamed into [Locator.Text]", ReplaceWith("Locator.Text", "org.readium.r2.shared.publication.Locator")) +typealias LocatorText = org.readium.r2.shared.publication.Locator.Text + +@Deprecated("Moved to another package", ReplaceWith("Locator.Text", "org.readium.r2.shared.publication.html.DomRange")) +typealias DomRange = org.readium.r2.shared.publication.html.DomRange + +@Deprecated("Renamed into [DomRange.Point]", ReplaceWith("DomRange.Point", "org.readium.r2.shared.publication.html.DomRange")) +typealias Range = org.readium.r2.shared.publication.html.DomRange.Point @Deprecated("Refactored into [LocalizedString]", ReplaceWith("org.readium.r2.shared.publication.LocalizedString")) typealias MultilanguageString = org.readium.r2.shared.publication.LocalizedString diff --git a/r2-shared/src/main/java/org/readium/r2/shared/Locator.kt b/r2-shared/src/main/java/org/readium/r2/shared/Locator.kt deleted file mode 100644 index eb493b71..00000000 --- a/r2-shared/src/main/java/org/readium/r2/shared/Locator.kt +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Aferdita Muriqi, Mostapha Idoubihi, Paul Stoica - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared - -import org.json.JSONObject -import java.io.Serializable - -/** - * Locator model - https://github.com/readium/architecture/tree/master/locators - * - * @val href: String - The href of the resource the locator points at. - * @val type: String - The media type of the resource that the Locator Object points to. - * @val title: String - The title of the chapter or section which is more relevant in the context of this locator. - * @val location: Location - One or more alternative expressions of the location. - * @val text: LocatorText? - Textual context of the locator. - */ - -open class Locator(var href: String? = null, - var type: String? = null, - var title: String? = null, - var locations: Locations? = null, - var text: LocatorText? = null) : JSONable, Serializable { - - - companion object { - fun fromJSON(json: JSONObject): Locator { - - val locator = Locator() - if (json.has("href")) { - locator.href = json.getString("href") - } - if (json.has("type")) { - locator.type = json.getString("type") - } - if (json.has("title")) { - locator.title = json.getString("title") - } - if (json.has("locations")) { - locator.locations = Locations.fromJSON(JSONObject(json.getString("locations"))) - } - if (json.has("text")) { - locator.text = LocatorText.fromJSON(JSONObject(json.getString("text"))) - } - - return locator - } - } - - override fun toJSON(): JSONObject { - val json = JSONObject() - - href.let { - json.putOpt("href", href) - } - type.let { - json.putOpt("type", type) - } - title.let { - json.putOpt("title", title) - } - locations?.let { - json.putOpt("locations", it.toJSON()) - } - text?.let { - json.putOpt("text", it.toJSON()) - } - return json - } - -} - -class LocatorText(var after: String? = null, - var before: String? = null, - var highlight: String? = null) - : JSONable, Serializable { - - companion object { - fun fromJSON(json: JSONObject): LocatorText { - - val location = LocatorText() - if (json.has("before")) { - location.before = json.getString("before") - } - if (json.has("highlight")) { - location.highlight = json.getString("highlight") - } - if (json.has("after")) { - location.after = json.getString("after") - } - - return location - } - } - - override fun toJSON(): JSONObject { - val json = JSONObject() - - before?.let { - json.putOpt("before", before) - } - highlight?.let { - json.putOpt("highlight", highlight) - } - after?.let { - json.putOpt("after", after) - } - - return json - } - -} - -/** - * Location : Class that contain the different variables needed to localize a particular position - * - * @var fragment: Long? - Contains one or more fragment in the resource referenced by the Locator Object. - * @var progression: Double - Progression in the resource expressed as a percentage. - * @var position: Long - An index in the publication. - * - */ -class Locations(var fragment: String? = null, // 1 = fragment identifier (toc, page lists, landmarks) - var progression: Double? = null, // 2 = bookmarks - var position: Long? = null, // 3 = goto page - var cssSelector: String? = null, - var partialCfi: String? = null, - var domRange: DomRange? = null -) : JSONable, Serializable { - - companion object { - fun fromJSON(json: JSONObject): Locations { - - val location = Locations() - if (json.has("fragment")) { - location.fragment = json.getString("fragment") - } - if (json.has("progression")) { - location.progression = json.getDouble("progression") - } - if (json.has("position")) { - location.position = json.getLong("position") - } - if (json.has("cssSelector")) { - location.cssSelector = json.getString("cssSelector") - } - if (json.has("partialCfi")) { - location.partialCfi = json.getString("partialCfi") - } - if (json.has("domRange")) { - location.domRange = DomRange.fromJSON(JSONObject(json.getString("domRange"))) - } - return location - } - - fun isEmpty(locations: Locations): Boolean { - if (locations.fragment == null && locations.position == null && locations.progression == null && locations.cssSelector == null && locations.partialCfi == null && locations.domRange == null) { - return true - } - return false - } - - } - - override fun toJSON(): JSONObject { - val json = JSONObject() - - fragment?.let { - json.putOpt("fragment", fragment) - } - progression?.let { - json.putOpt("progression", progression) - } - position?.let { - json.putOpt("position", position) - } - cssSelector?.let { - json.putOpt("cssSelector", cssSelector) - } - partialCfi?.let { - json.putOpt("partialCfi", partialCfi) - } - domRange?.let { - json.putOpt("domRange", it.toJSON()) - } - - return json - } -} - -class Range(var cssSelector: String? = null, - var textNodeIndex: Long? = null, - var offset: Long? = null -) : JSONable, Serializable { - - companion object { - fun fromJSON(json: JSONObject): Range { - - val range = Range() - if (json.has("cssSelector")) { - range.cssSelector = json.getString("cssSelector") - } - if (json.has("textNodeIndex")) { - range.textNodeIndex = json.getLong("textNodeIndex") - } - if (json.has("offset")) { - range.offset = json.getLong("offset") - } - return range - } - - fun isEmpty(locations: Range): Boolean { - if (locations.cssSelector == null && locations.textNodeIndex == null && locations.offset == null) { - return true - } - return false - } - - } - - override fun toJSON(): JSONObject { - val json = JSONObject() - - cssSelector?.let { - json.putOpt("cssSelector", cssSelector) - } - textNodeIndex?.let { - json.putOpt("textNodeIndex", textNodeIndex) - } - offset?.let { - json.putOpt("offset", offset) - } - - return json - } -} - - -class DomRange(var start: Range? = null, - var end: Range? = null -) : JSONable, Serializable { - - companion object { - fun fromJSON(json: JSONObject): DomRange { - - val domRange = DomRange() - - if (json.has("start")) { - domRange.start = Range.fromJSON(JSONObject(json.getString("start"))) - } - if (json.has("end")) { - domRange.end = Range.fromJSON(JSONObject(json.getString("end"))) - } - - return domRange - } - - fun isEmpty(locations: DomRange): Boolean { - if (locations.start == null && locations.end == null) { - return true - } - return false - } - - } - - override fun toJSON(): JSONObject { - val json = JSONObject() - - start?.let { - json.putOpt("start", it.toJSON()) - } - end?.let { - json.putOpt("end", it.toJSON()) - } - - return json - } - -} diff --git a/r2-shared/src/main/java/org/readium/r2/shared/extensions/JSON.kt b/r2-shared/src/main/java/org/readium/r2/shared/extensions/JSON.kt index c5a8a222..b7ac824b 100644 --- a/r2-shared/src/main/java/org/readium/r2/shared/extensions/JSON.kt +++ b/r2-shared/src/main/java/org/readium/r2/shared/extensions/JSON.kt @@ -111,7 +111,7 @@ internal fun JSONObject.putIfNotEmpty(name: String, collection: Collection<*>) { */ fun JSONObject.optPositiveInt(name: String, fallback: Int = -1, remove: Boolean = false): Int? { val int = optInt(name, fallback) - val value = if (int > 0) int else null + val value = if (int >= 0) int else null if (remove) { this.remove(name) } @@ -125,7 +125,7 @@ fun JSONObject.optPositiveInt(name: String, fallback: Int = -1, remove: Boolean */ fun JSONObject.optPositiveDouble(name: String, fallback: Double = -1.0, remove: Boolean = false): Double? { val double = optDouble(name, fallback) - val value = if (double > 0) double else null + val value = if (double >= 0) double else null if (remove) { this.remove(name) } diff --git a/r2-shared/src/main/java/org/readium/r2/shared/publication/Locator.kt b/r2-shared/src/main/java/org/readium/r2/shared/publication/Locator.kt new file mode 100644 index 00000000..37909b97 --- /dev/null +++ b/r2-shared/src/main/java/org/readium/r2/shared/publication/Locator.kt @@ -0,0 +1,198 @@ +/* + * Module: r2-shared-kotlin + * Developers: Mickaël Menu + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.shared.publication + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize +import kotlinx.android.parcel.WriteWith +import org.json.JSONObject +import org.readium.r2.shared.JSONable +import org.readium.r2.shared.extensions.* +import org.readium.r2.shared.extensions.putIfNotEmpty +import org.readium.r2.shared.util.logging.JsonWarning +import org.readium.r2.shared.util.logging.WarningLogger +import org.readium.r2.shared.util.logging.log + +/** + * Provides a precise location in a publication in a format that can be stored and shared. + * + * There are many different use cases for locators: + * - getting back to the last position in a publication + * - bookmarks + * - highlights & annotations + * - search results + * - human-readable (and shareable) reference in a publication + * + * https://github.com/readium/architecture/tree/master/locators + */ +@Parcelize +data class Locator( + val href: String, + val type: String, + val title: String? = null, + val locations: Locations = Locations(), + val text: Text = Text() +) : JSONable, Parcelable { + + /** + * One or more alternative expressions of the location. + * https://github.com/readium/architecture/tree/master/models/locators#the-location-object + * + * @param fragments Contains one or more fragment in the resource referenced by the [Locator]. + * @param progression Progression in the resource expressed as a percentage (between 0 and 1). + * @param position An index in the publication (>= 1). + * @param totalProgression Progression in the publication expressed as a percentage (between 0 + * and 1). + * @param otherLocations Additional locations for extensions. + */ + @Parcelize + data class Locations( + val fragments: List = emptyList(), + val progression: Double? = null, + val position: Int? = null, + val totalProgression: Double? = null, + val otherLocations: @WriteWith Map = emptyMap() + ) : JSONable, Parcelable { + + override fun toJSON() = JSONObject(otherLocations).apply { + putIfNotEmpty("fragments", fragments) + put("progression", progression) + put("position", position) + put("totalProgression", totalProgression) + } + + /** + * Syntactic sugar to access the [otherLocations] values by subscripting [Locations] directly. + * `locations["cssSelector"] == locations.otherLocations["cssSelector"]` + */ + operator fun get(key: String): Any? = otherLocations[key] + + companion object { + + fun fromJSON(json: JSONObject?): Locations { + val fragments = json?.optStringsFromArrayOrSingle("fragments", remove = true)?.takeIf { it.isNotEmpty() } + ?: json?.optStringsFromArrayOrSingle("fragment", remove = true) + ?: emptyList() + + val progression = json?.optNullableDouble("progression", remove = true) + ?.takeIf { it in 0.0..1.0 } + + val position = json?.optNullableInt("position", remove = true) + ?.takeIf { it > 0 } + + val totalProgression = json?.optNullableDouble("totalProgression", remove = true) + ?.takeIf { it in 0.0..1.0 } + + return Locations( + fragments = fragments, + progression = progression, + position = position, + totalProgression = totalProgression, + otherLocations = json?.toMap() ?: emptyMap() + ) + } + + } + + @Deprecated("Renamed to [fragments]", ReplaceWith("fragments")) + val fragment: String? get() = fragments.firstOrNull() + + } + + /** + * Textual context of the locator. + * + * A Locator Text Object contains multiple text fragments, useful to give a context to the + * [Locator] or for highlights. + * https://github.com/readium/architecture/tree/master/models/locators#the-text-object + * + * @param before The text before the locator. + * @param highlight The text at the locator. + * @param after The text after the locator. + */ + @Parcelize + data class Text( + val before: String? = null, + val highlight: String? = null, + val after: String? = null + ) : JSONable, Parcelable { + + override fun toJSON() = JSONObject().apply { + put("before", before) + put("highlight", highlight) + put("after", after) + } + + companion object { + + fun fromJSON(json: JSONObject?) = Text( + before = json?.optNullableString("before"), + highlight = json?.optNullableString("highlight"), + after = json?.optNullableString("after") + ) + + } + + } + + /** + * Returns a copy of the [Locator] after setting the [Locations.progression] to [progression]. + */ + fun withProgression(progression: Double?) = copy( + locations = locations.copy( + progression = progression + ) + ) + + override fun toJSON() = JSONObject().apply { + put("href", href) + put("type", type) + put("title", title) + putIfNotEmpty("locations", locations) + putIfNotEmpty("text", text) + } + + companion object { + + fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Locator? { + val href = json?.optNullableString("href") + val type = json?.optNullableString("type") + if (href == null || type == null) { + warnings?.log(Locator::class.java, "[href] and [type] are required", json) + return null + } + + return Locator( + href = href, + type = type, + title = json.optNullableString("title"), + locations = Locations.fromJSON(json.optJSONObject("locations")), + text = Text.fromJSON(json.optJSONObject("text")) + ) + } + + } + +} + +/** + * Creates a [Locator] from a reading order [Link]. + */ +fun Link.toLocator(): Locator { + val components = href.split("#", limit = 2) + return Locator( + href = components.firstOrNull() ?: href, + type = type ?: "", + title = title, + locations = Locator.Locations( + fragments = listOfNotNull(components.getOrNull(1)) + ) + ) +} diff --git a/r2-shared/src/main/java/org/readium/r2/shared/publication/html/DomRange.kt b/r2-shared/src/main/java/org/readium/r2/shared/publication/html/DomRange.kt new file mode 100644 index 00000000..dd67a39a --- /dev/null +++ b/r2-shared/src/main/java/org/readium/r2/shared/publication/html/DomRange.kt @@ -0,0 +1,122 @@ +/* + * Module: r2-shared-kotlin + * Developers: Mickaël Menu + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.shared.publication.html + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize +import org.json.JSONObject +import org.readium.r2.shared.JSONable +import org.readium.r2.shared.extensions.optNullableString +import org.readium.r2.shared.extensions.optPositiveInt +import org.readium.r2.shared.extensions.putIfNotEmpty +import org.readium.r2.shared.util.logging.JsonWarning +import org.readium.r2.shared.util.logging.WarningLogger +import org.readium.r2.shared.util.logging.log + +/** + * This construct enables a serializable representation of a DOM Range. + * + * In a DOM Range object, the startContainer + startOffset tuple represents the [start] boundary + * point. Similarly, the the endContainer + endOffset tuple represents the [end] boundary point. + * In both cases, the start/endContainer property is a pointer to either a DOM text node, or a DOM + * element (this typically depends on the mechanism from which the DOM Range instance originates, + * for example when obtaining the currently-selected document fragment using the `window.selection` + * API). In the case of a DOM text node, the start/endOffset corresponds to a position within the + * character data. In the case of a DOM element node, the start/endOffset corresponds to a position + * that designates a child text node. + * + * Note that [end] field is optional. When only the start field is specified, the domRange object + * represents a "collapsed" range that has identical [start] and [end] boundary points. + * + * https://github.com/readium/architecture/blob/master/models/locators/extensions/html.md#the-domrange-object + * + * @param start A serializable representation of the "start" boundary point of the DOM Range. + * @param end A serializable representation of the "end" boundary point of the DOM Range. + */ +@Parcelize +data class DomRange( + val start: Point, + val end: Point? = null +) : JSONable, Parcelable { + + /** + * A serializable representation of a boundary point in a DOM Range. + * + * The [cssSelector] field always references a DOM element. If the original DOM Range + * start/endContainer property references a DOM text node, the [textNodeIndex] field is used to + * complement the CSS Selector; thereby providing a pointer to a child DOM text node; and + * [charOffset] is used to tell a position within the character data of that DOM text node + * (just as the DOM Range start/endOffset does). If the original DOM Range start/endContainer + * property references a DOM Element, then the [textNodeIndex] field is used to designate the + * child Text node (just as the DOM Range start/endOffset does), and the optional [charOffset] + * field is not used (as there is no explicit position within the character data of the text + * node). + * + * https://github.com/readium/architecture/blob/master/models/locators/extensions/html.md#the-start-and-end-object + */ + @Parcelize + data class Point( + val cssSelector: String, + val textNodeIndex: Int, + val charOffset: Int? = null + ) : JSONable, Parcelable { + + override fun toJSON() = JSONObject().apply { + put("cssSelector", cssSelector) + put("textNodeIndex", textNodeIndex) + put("charOffset", charOffset) + } + + companion object { + + fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Point? { + val cssSelector = json?.optNullableString("cssSelector") + val textNodeIndex = json?.optPositiveInt("textNodeIndex") + if (cssSelector == null || textNodeIndex == null) { + warnings?.log(Point::class.java, "[cssSelector] and [textNodeIndex] are required", json) + return null + } + + return Point( + cssSelector = cssSelector, + textNodeIndex = textNodeIndex, + charOffset = json.optPositiveInt("charOffset") + ) + } + + } + + @Deprecated("Renamed into [charOffset]", ReplaceWith("charOffset")) + val offset: Long? get() = charOffset?.toLong() + } + + override fun toJSON() = JSONObject().apply { + putIfNotEmpty("start", start) + putIfNotEmpty("end", end) + } + + companion object { + + fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): DomRange? { + val start = Point.fromJSON(json?.optJSONObject("start")) + if (start == null) { + warnings?.log(DomRange::class.java, "[start] is required", json) + return null + } + + return DomRange( + start = start, + end = Point.fromJSON(json?.optJSONObject("end")) + ) + } + + } + +} diff --git a/r2-shared/src/main/java/org/readium/r2/shared/publication/html/Locator.kt b/r2-shared/src/main/java/org/readium/r2/shared/publication/html/Locator.kt new file mode 100644 index 00000000..9d0d517c --- /dev/null +++ b/r2-shared/src/main/java/org/readium/r2/shared/publication/html/Locator.kt @@ -0,0 +1,39 @@ +/* + * Module: r2-shared-kotlin + * Developers: Mickaël Menu + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.shared.publication.html + +import org.json.JSONObject +import org.readium.r2.shared.publication.Locator + +// HTML extensions for [Locator.Locations]. +// https://github.com/readium/architecture/blob/master/models/locators/extensions/html.md + +/** + * A CSS Selector. + */ +val Locator.Locations.cssSelector: String? + get() = this["cssSelector"] as? String + +/** + * [partialCfi] is an expression conforming to the "right-hand" side of the EPUB CFI syntax, that is + * to say: without the EPUB-specific OPF spine item reference that precedes the first ! exclamation + * mark (which denotes the "step indirection" into a publication document). Note that the wrapping + * epubcfi(***) syntax is not used for the [partialCfi] string, i.e. the "fragment" part of the CFI + * grammar is ignored. + */ +val Locator.Locations.partialCfi: String? + get() = this["partialCfi"] as? String + +/** + * An HTML DOM range. + */ +val Locator.Locations.domRange: DomRange? + get() = (this["domRange"] as? Map<*, *>) + ?.let { DomRange.fromJSON(JSONObject(it)) } diff --git a/r2-shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt b/r2-shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt new file mode 100644 index 00000000..69b1563f --- /dev/null +++ b/r2-shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt @@ -0,0 +1,264 @@ +/* + * Module: r2-shared-kotlin + * Developers: Mickaël Menu + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.shared.publication + +import org.json.JSONObject +import org.junit.Assert.* +import org.junit.Test +import org.readium.r2.shared.assertJSONEquals + +class LocatorTest { + + @Test fun `parse {Locator} minimal JSON`() { + assertEquals( + Locator(href = "http://locator", type = "text/html"), + Locator.fromJSON(JSONObject("""{ + "href": "http://locator", + "type": "text/html" + }""")) + ) + } + + @Test fun `parse {Locator} full JSON`() { + assertEquals( + Locator( + href = "http://locator", + type = "text/html", + title = "My Locator", + locations = Locator.Locations(position = 42), + text = Locator.Text(highlight = "Excerpt") + ), + Locator.fromJSON(JSONObject("""{ + "href": "http://locator", + "type": "text/html", + "title": "My Locator", + "locations": { + "position": 42 + }, + "text": { + "highlight": "Excerpt" + } + }""")) + ) + } + + @Test fun `parse {Locator} null JSON`() { + assertEquals(null, Locator.fromJSON(null)) + } + + @Test fun `parse {Locator} invalid JSON`() { + assertNull(Locator.fromJSON(JSONObject("{ 'invalid': 'object' }"))) + } + + @Test fun `create {Locator} from minimal {Link}`() { + assertEquals( + Locator(href = "http://locator", type = ""), + Link(href = "http://locator").toLocator() + ) + } + + @Test fun `create {Locator} from full {Link} with fragment`() { + assertEquals( + Locator( + href = "http://locator", + type = "text/html", + title = "My Link", + locations = Locator.Locations(fragments = listOf("page=42")) + ), + Link( + href = "http://locator#page=42", + type = "text/html", + title = "My Link" + ).toLocator() + ) + } + + @Test fun `get {Locator} minimal JSON`() { + assertJSONEquals( + JSONObject("""{ + "href": "http://locator", + "type": "text/html" + }"""), + Locator(href = "http://locator", type = "text/html").toJSON() + ) + } + + @Test fun `get {Locator} full JSON`() { + assertJSONEquals( + JSONObject("""{ + "href": "http://locator", + "type": "text/html", + "title": "My Locator", + "locations": { + "position": 42 + }, + "text": { + "highlight": "Excerpt" + } + }"""), + Locator( + href = "http://locator", + type = "text/html", + title = "My Locator", + locations = Locator.Locations(position = 42), + text = Locator.Text(highlight = "Excerpt") + ).toJSON() + ) + } + + @Test fun `copy a {Locator} with a new {progression}`() { + assertEquals( + Locator( + href = "http://locator", + type = "text/html", + locations = Locator.Locations(position = 42, progression = 4.1) + ), + Locator( + href = "http://locator", + type = "text/html", + locations = Locator.Locations(position = 42, progression = 2.0) + ).withProgression(4.1) + ) + } + + @Test fun `parse {Locations} minimal JSON`() { + assertEquals( + Locator.Locations(), + Locator.Locations.fromJSON(JSONObject("{}")) + ) + } + + @Test fun `parse {Locations} full JSON`() { + assertEquals( + Locator.Locations( + fragments = listOf("p=4", "frag34"), + progression = 0.74, + position = 42, + totalProgression = 0.32, + otherLocations = mapOf("other" to "other-location") + ), + Locator.Locations.fromJSON(JSONObject("""{ + "fragments": ["p=4", "frag34"], + "progression": 0.74, + "totalProgression": 0.32, + "position": 42, + "other": "other-location" + }""")) + ) + } + + @Test fun `parse {Locations} null JSON`() { + assertEquals(Locator.Locations(), Locator.Locations.fromJSON(null)) + } + + @Test fun `parse {Locations} single fragment JSON`() { + assertEquals( + Locator.Locations(fragments = listOf("frag34")), + Locator.Locations.fromJSON(JSONObject("{ 'fragment': 'frag34' }")) + ) + } + + @Test fun `parse {Locations} ignores {position} smaller than 1`() { + assertEquals(Locator.Locations(position = 1), Locator.Locations.fromJSON(JSONObject("{ 'position': 1 }"))) + assertEquals(Locator.Locations(), Locator.Locations.fromJSON(JSONObject("{ 'position': 0 }"))) + assertEquals(Locator.Locations(), Locator.Locations.fromJSON(JSONObject("{ 'position': -1 }"))) + } + + @Test fun `parse {Locations} ignores {progression} outside of 0-1 range`() { + assertEquals(Locator.Locations(progression = 0.5), Locator.Locations.fromJSON(JSONObject("{ 'progression': 0.5 }"))) + assertEquals(Locator.Locations(progression = 0.0), Locator.Locations.fromJSON(JSONObject("{ 'progression': 0 }"))) + assertEquals(Locator.Locations(progression = 1.0), Locator.Locations.fromJSON(JSONObject("{ 'progression': 1 }"))) + assertEquals(Locator.Locations(), Locator.Locations.fromJSON(JSONObject("{ 'progression': -0.5 }"))) + assertEquals(Locator.Locations(), Locator.Locations.fromJSON(JSONObject("{ 'progression': 1.2 }"))) + } + + @Test fun `parse {Locations} ignores {totalProgression} outside of 0-1 range`() { + assertEquals(Locator.Locations(totalProgression = 0.5), Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': 0.5 }"))) + assertEquals(Locator.Locations(totalProgression = 0.0), Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': 0 }"))) + assertEquals(Locator.Locations(totalProgression = 1.0), Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': 1 }"))) + assertEquals(Locator.Locations(), Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': -0.5 }"))) + assertEquals(Locator.Locations(), Locator.Locations.fromJSON(JSONObject("{ 'totalProgression': 1.2 }"))) + } + + @Test fun `get {Locations} minimal JSON`() { + assertJSONEquals( + JSONObject("{}"), + Locator.Locations().toJSON() + ) + } + + @Test fun `get {Locations} full JSON`() { + assertJSONEquals( + JSONObject("""{ + "fragments": ["p=4", "frag34"], + "progression": 0.74, + "totalProgression": 25.32, + "position": 42, + "other": "other-location" + }"""), + Locator.Locations( + fragments = listOf("p=4", "frag34"), + progression = 0.74, + position = 42, + totalProgression = 25.32, + otherLocations = mapOf("other" to "other-location") + ).toJSON() + ) + } + + @Test fun `parse {Text} minimal JSON`() { + assertEquals( + Locator.Text(), + Locator.Text.fromJSON(JSONObject("{}")) + ) + } + + @Test fun `parse {Text} full JSON`() { + assertEquals( + Locator.Text( + before = "Text before", + highlight = "Highlighted text", + after = "Text after" + ), + Locator.Text.fromJSON(JSONObject("""{ + "before": "Text before", + "highlight": "Highlighted text", + "after": "Text after" + }""")) + ) + } + + @Test fun `parse {Text} null JSON`() { + assertEquals(Locator.Text(), Locator.Text.fromJSON(null)) + } + + @Test fun `get {Text} minimal JSON`() { + assertJSONEquals( + JSONObject("{}"), + Locator.Text().toJSON() + ) + } + + @Test fun `get {Text} full JSON`() { + assertJSONEquals( + JSONObject("""{ + "before": "Text before", + "highlight": "Highlighted text", + "after": "Text after" + }"""), + Locator.Text( + before = "Text before", + highlight = "Highlighted text", + after = "Text after" + ).toJSON() + ) + } + +} diff --git a/r2-shared/src/test/java/org/readium/r2/shared/publication/html/DomRangeTest.kt b/r2-shared/src/test/java/org/readium/r2/shared/publication/html/DomRangeTest.kt new file mode 100644 index 00000000..07d5bb36 --- /dev/null +++ b/r2-shared/src/test/java/org/readium/r2/shared/publication/html/DomRangeTest.kt @@ -0,0 +1,190 @@ +/* + * Module: r2-shared-kotlin + * Developers: Mickaël Menu + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.shared.publication.html + +import org.json.JSONObject +import org.junit.Test +import org.readium.r2.shared.assertJSONEquals +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class DomRangeTest { + + @Test fun `parse {DomRange} minimal JSON`() { + assertEquals( + DomRange(start = DomRange.Point(cssSelector = "p", textNodeIndex = 4)), + DomRange.fromJSON(JSONObject("""{ + "start": { + "cssSelector": "p", + "textNodeIndex": 4 + } + }""")) + ) + } + + @Test fun `parse {DomRange} full JSON`() { + assertEquals( + DomRange( + start = DomRange.Point(cssSelector = "p", textNodeIndex = 4), + end = DomRange.Point(cssSelector = "a", textNodeIndex = 2) + ), + DomRange.fromJSON(JSONObject("""{ + "start": { + "cssSelector": "p", + "textNodeIndex": 4 + }, + "end": { + "cssSelector": "a", + "textNodeIndex": 2 + } + }""")) + ) + } + + @Test fun `parse {DomRange} invalid JSON`() { + assertNull(DomRange.fromJSON(JSONObject("{ 'invalid': 'object' }"))) + } + + @Test fun `parse {DomRange} null JSON`() { + assertNull(DomRange.fromJSON(null)) + } + + @Test fun `get {DomRange} minimal JSON`() { + assertJSONEquals( + JSONObject("""{ + "start": { + "cssSelector": "p", + "textNodeIndex": 4 + } + }"""), + DomRange(start = DomRange.Point(cssSelector = "p", textNodeIndex = 4)).toJSON() + ) + } + + @Test fun `get {DomRange} full JSON`() { + assertJSONEquals( + JSONObject("""{ + "start": { + "cssSelector": "p", + "textNodeIndex": 4 + }, + "end": { + "cssSelector": "a", + "textNodeIndex": 2 + } + }"""), + DomRange( + start = DomRange.Point(cssSelector = "p", textNodeIndex = 4), + end = DomRange.Point(cssSelector = "a", textNodeIndex = 2) + ).toJSON() + ) + } + + @Test fun `parse {Point} minimal JSON`() { + assertEquals( + DomRange.Point(cssSelector = "p", textNodeIndex = 4), + DomRange.Point.fromJSON(JSONObject("""{ + "cssSelector": "p", + "textNodeIndex": 4 + }""")) + ) + } + + @Test fun `parse {Point} full JSON`() { + assertEquals( + DomRange.Point(cssSelector = "p", textNodeIndex = 4, charOffset = 32), + DomRange.Point.fromJSON(JSONObject("""{ + "cssSelector": "p", + "textNodeIndex": 4, + "charOffset": 32 + }""")) + ) + } + + @Test fun `parse {Point} invalid JSON`() { + assertNull(DomRange.Point.fromJSON(JSONObject("""{ + "cssSelector": "p" + }"""))) + } + + @Test fun `parse {Point} null JSON`() { + assertNull(DomRange.Point.fromJSON(null)) + } + + @Test fun `parse {Point} requires positive {textNodeIndex}`() { + assertEquals( + DomRange.Point(cssSelector = "p", textNodeIndex = 1), + DomRange.Point.fromJSON(JSONObject("""{ + "cssSelector": "p", + "textNodeIndex": 1 + }""")) + ) + assertEquals( + DomRange.Point(cssSelector = "p", textNodeIndex = 0), + DomRange.Point.fromJSON(JSONObject("""{ + "cssSelector": "p", + "textNodeIndex": 0 + }""")) + ) + assertNull(DomRange.fromJSON(JSONObject("""{ + "cssSelector": "p", + "textNodeIndex": -1 + }"""))) + } + + @Test fun `parse {Point} requires positive {charOffset}`() { + assertEquals( + DomRange.Point(cssSelector = "p", textNodeIndex = 1, charOffset = 1), + DomRange.Point.fromJSON(JSONObject("""{ + "cssSelector": "p", + "textNodeIndex": 1, + "charOffset": 1 + }""")) + ) + assertEquals( + DomRange.Point(cssSelector = "p", textNodeIndex = 1, charOffset = 0), + DomRange.Point.fromJSON(JSONObject("""{ + "cssSelector": "p", + "textNodeIndex": 1, + "charOffset": 0 + }""")) + ) + assertEquals( + DomRange.Point(cssSelector = "p", textNodeIndex = 1), + DomRange.Point.fromJSON(JSONObject("""{ + "cssSelector": "p", + "textNodeIndex": 1, + "charOffset": -1 + }""")) + ) + } + + @Test fun `get {Point} minimal JSON`() { + assertJSONEquals( + JSONObject("""{ + "cssSelector": "p", + "textNodeIndex": 4 + }"""), + DomRange.Point(cssSelector = "p", textNodeIndex = 4).toJSON() + ) + } + + @Test fun `get {Point} full JSON`() { + assertJSONEquals( + JSONObject("""{ + "cssSelector": "p", + "textNodeIndex": 4, + "charOffset": 32 + }"""), + DomRange.Point(cssSelector = "p", textNodeIndex = 4, charOffset = 32).toJSON() + ) + } + +} diff --git a/r2-shared/src/test/java/org/readium/r2/shared/publication/html/LocatorTest.kt b/r2-shared/src/test/java/org/readium/r2/shared/publication/html/LocatorTest.kt new file mode 100644 index 00000000..80d46f24 --- /dev/null +++ b/r2-shared/src/test/java/org/readium/r2/shared/publication/html/LocatorTest.kt @@ -0,0 +1,58 @@ +/* + * Module: r2-shared-kotlin + * Developers: Mickaël Menu + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.shared.publication.html + +import org.junit.Assert.* +import org.junit.Test +import org.readium.r2.shared.publication.Locator + +class LocatorTest { + + @Test fun `get Locations {cssSelector} when available`() { + assertEquals( + "p", + Locator.Locations(otherLocations = mapOf("cssSelector" to "p")).cssSelector + ) + } + + @Test fun `get Locations {cssSelector} when missing`() { + assertNull(Locator.Locations().cssSelector) + } + + @Test fun `get Locations {partialCfi} when available`() { + assertEquals( + "epubcfi(/4)", + Locator.Locations(otherLocations = mapOf("partialCfi" to "epubcfi(/4)")).partialCfi + ) + } + + @Test fun `get Locations {partialCfi} when missing`() { + assertNull(Locator.Locations().partialCfi) + } + + @Test fun `get Locations {domRange} when available`() { + assertEquals( + DomRange(start = DomRange.Point(cssSelector = "p", textNodeIndex = 4)), + Locator.Locations(otherLocations = mapOf( + "domRange" to mapOf( + "start" to mapOf( + "cssSelector" to "p", + "textNodeIndex" to 4 + ) + ) + )).domRange + ) + } + + @Test fun `get Locations {domRange} when missing`() { + assertNull(Locator.Locations().domRange) + } + +} \ No newline at end of file From e8454480aa52d0c76f8d700ab8852cf38d536ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Fri, 7 Feb 2020 19:11:36 +0100 Subject: [PATCH 2/8] Add the positionList in Publication --- r2-shared/build.gradle | 19 ++++---- .../r2/shared/publication/Publication.kt | 29 ++++++++++++ .../PublicationParcelizationTest.kt | 47 +++++++++++++++++++ .../r2/shared/publication/PublicationTest.kt | 41 +++++++++++++++- 4 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationParcelizationTest.kt diff --git a/r2-shared/build.gradle b/r2-shared/build.gradle index 204b1114..3d72f788 100644 --- a/r2-shared/build.gradle +++ b/r2-shared/build.gradle @@ -51,13 +51,16 @@ dependencies { implementation 'nl.komponents.kovenant:kovenant-functional:3.3.0' implementation 'joda-time:joda-time:2.9.9' + testImplementation "androidx.test:core-ktx:1.2.0" + testImplementation "androidx.test.ext:junit-ktx:1.1.1" + testImplementation "junit:junit:4.12" + testImplementation "net.sf.kxml:kxml2:2.3.0" testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.19.0' - testImplementation 'xmlpull:xmlpull:1.1.3.1' - testImplementation 'net.sf.kxml:kxml2:2.3.0' - testImplementation 'org.json:json:20190722' - - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + testImplementation "org.json:json:20190722" + testImplementation "org.mockito:mockito-core:2.19.0" + testImplementation "org.robolectric:robolectric:4.3.1" + testImplementation "xmlpull:xmlpull:1.1.3.1" + + androidTestImplementation "androidx.test:runner:1.2.0" + androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0" } diff --git a/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index a09640e4..d4e42929 100644 --- a/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -11,6 +11,7 @@ package org.readium.r2.shared.publication import android.net.Uri import android.os.Parcelable +import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.android.parcel.Parcelize import org.json.JSONArray import org.json.JSONObject @@ -24,13 +25,20 @@ import org.readium.r2.shared.publication.epub.listOfAudioClips import org.readium.r2.shared.publication.epub.listOfVideoClips import org.readium.r2.shared.util.logging.JsonWarning import org.readium.r2.shared.util.logging.log +import java.io.Serializable import java.net.URL +typealias PositionListFactory = (Publication) -> List + /** * Shared model for a Readium Publication. * * @param type The kind of publication it is ( Epub, Cbz, ... ) * @param version The version of the publication, if the type needs any. + * @param positionListFactory Factory used to build lazily the [positionList]. By default, a parser + * will set this to parse the [positionList] from the publication, but the host app might + * want to overwrite this with a custom closure to implement, for example, a caching + * mechanism. */ @Parcelize data class Publication( @@ -43,6 +51,9 @@ data class Publication( val resources: List = emptyList(), val tableOfContents: List = emptyList(), val otherCollections: List = emptyList(), + // FIXME: This is a Serializable to be able to use Parcelize, but this should be changed once we moved away from Activity – see https://github.com/readium/r2-navigator-kotlin/issues/115 + // Should be a [PositionListFactory] + val positionListFactory: Serializable = { emptyList() } as Serializable, // FIXME: To be refactored, with the TYPE and EXTENSION enums as well var type: TYPE = TYPE.EPUB, @@ -78,6 +89,22 @@ data class Publication( } } + /** + * List of all the positions in the publication. + */ + @IgnoredOnParcel + val positionList: List by lazy { + (positionListFactory as? PositionListFactory)?.invoke(this) ?: emptyList() + } + + /** + * List of all the positions in each resource, indexed by their [href]. + */ + @IgnoredOnParcel + val positionListByResource: Map> by lazy { + positionList.groupBy { it.href } + } + /** * Returns the RWPM JSON representation for this manifest, as a string. */ @@ -247,9 +274,11 @@ data class Publication( } + @IgnoredOnParcel @Deprecated("Renamed to [listOfAudioClips]", ReplaceWith("listOfAudioClips")) val listOfAudioFiles: List = listOfAudioClips + @IgnoredOnParcel @Deprecated("Renamed to [listOfVideoClips]", ReplaceWith("listOfVideoClips")) val listOfVideos: List = listOfVideoClips diff --git a/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationParcelizationTest.kt b/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationParcelizationTest.kt new file mode 100644 index 00000000..05546988 --- /dev/null +++ b/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationParcelizationTest.kt @@ -0,0 +1,47 @@ +/* + * Module: r2-shared-kotlin + * Developers: Mickaël Menu + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.shared.publication + +import android.content.Intent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import java.io.Serializable + +// FIXME: To be removed once we migrate from Activity to Fragment, then serializing the Publication won't be necessary anymore – see https://github.com/readium/r2-navigator-kotlin/issues/115 +// Commented because running tests with Roboletric is too slow +//@RunWith(AndroidJUnit4::class) +//class PublicationParcelizationTest { +// +// private fun createPublication( +// positionListFactory: PositionListFactory = { emptyList() } +// ) = Publication( +// metadata = Metadata( +// localizedTitle = LocalizedString("Title") +// ), +// positionListFactory = positionListFactory as Serializable +// ) +// +// @Test fun `parcelization of the {positionListFactory}`() { +// var publication = createPublication( +// positionListFactory = { listOf(Locator(href="locator", type = "")) } +// ) +// val intent = Intent() +// intent.putExtra("publication", publication) +// publication = intent.getParcelableExtra("publication") as Publication +// +// assertEquals( +// listOf(Locator(href = "locator", type = "")), +// publication.positionList +// ) +// } +// +//} diff --git a/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt b/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt index c6b598c0..7e18115a 100644 --- a/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt +++ b/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt @@ -13,6 +13,7 @@ import org.json.JSONObject import org.junit.Assert.* import org.junit.Test import org.readium.r2.shared.assertJSONEquals +import java.io.Serializable import java.net.URL class PublicationTest { @@ -23,7 +24,8 @@ class PublicationTest { readingProgression: ReadingProgression = ReadingProgression.AUTO, links: List = listOf(), readingOrder: List = emptyList(), - resources: List = emptyList() + resources: List = emptyList(), + positionListFactory: PositionListFactory = { emptyList() } ) = Publication( metadata = Metadata( localizedTitle = LocalizedString(title), @@ -32,7 +34,8 @@ class PublicationTest { ), links = links, readingOrder = readingOrder, - resources = resources + resources = resources, + positionListFactory = positionListFactory as Serializable ) @Test fun `parse minimal JSON`() { @@ -266,6 +269,40 @@ class PublicationTest { ) } + @Test fun `get the default empty {positionList}`() { + assertEquals(emptyList(), createPublication().positionList) + } + + @Test fun `get the {positionList} computed from the {positionListFactory}`() { + assertEquals( + listOf(Locator(href = "locator", type = "")), + createPublication( + positionListFactory = { listOf(Locator(href="locator", type = "")) } + ).positionList + ) + } + + @Test fun `get the {positionListByResource} computed from the {positionListFactory}`() { + assertEquals( + mapOf( + "res1" to listOf( + Locator(href="res1", type = "text/html", title = "Loc A"), + Locator(href="res1", type = "text/html", title = "Loc B") + ), + "res2" to listOf( + Locator(href="res2", type = "text/html", title = "Loc B") + ) + ), + createPublication( + positionListFactory = { listOf( + Locator(href="res1", type = "text/html", title = "Loc A"), + Locator(href="res2", type = "text/html", title = "Loc B"), + Locator(href="res1", type = "text/html", title = "Loc B") + ) } + ).positionListByResource + ) + } + @Test fun `get {contentLayout} for the default language`() { assertEquals( ContentLayout.RTL, From d7bb1bcd1f08d073a4a1c1fdd0b5c43d47a8b4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Sun, 9 Feb 2020 21:47:39 +0100 Subject: [PATCH 3/8] Refactor PositionListFactory to use an interface instead of a closure --- .../r2/shared/publication/Publication.kt | 64 ++++++++++++++++--- .../r2/shared/publication/PublicationTest.kt | 21 +++--- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index d4e42929..d645c1b3 100644 --- a/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -10,9 +10,11 @@ package org.readium.r2.shared.publication import android.net.Uri +import android.os.Parcel import android.os.Parcelable import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.android.parcel.Parcelize +import kotlinx.android.parcel.WriteWith import org.json.JSONArray import org.json.JSONObject import org.readium.r2.shared.JSONable @@ -25,20 +27,16 @@ import org.readium.r2.shared.publication.epub.listOfAudioClips import org.readium.r2.shared.publication.epub.listOfVideoClips import org.readium.r2.shared.util.logging.JsonWarning import org.readium.r2.shared.util.logging.log +import timber.log.Timber import java.io.Serializable import java.net.URL -typealias PositionListFactory = (Publication) -> List - /** * Shared model for a Readium Publication. * * @param type The kind of publication it is ( Epub, Cbz, ... ) * @param version The version of the publication, if the type needs any. - * @param positionListFactory Factory used to build lazily the [positionList]. By default, a parser - * will set this to parse the [positionList] from the publication, but the host app might - * want to overwrite this with a custom closure to implement, for example, a caching - * mechanism. + * @param positionListFactory Factory used to build lazily the [positionList]. */ @Parcelize data class Publication( @@ -51,9 +49,7 @@ data class Publication( val resources: List = emptyList(), val tableOfContents: List = emptyList(), val otherCollections: List = emptyList(), - // FIXME: This is a Serializable to be able to use Parcelize, but this should be changed once we moved away from Activity – see https://github.com/readium/r2-navigator-kotlin/issues/115 - // Should be a [PositionListFactory] - val positionListFactory: Serializable = { emptyList() } as Serializable, + val positionListFactory: @WriteWith PositionListFactory? = null, // FIXME: To be refactored, with the TYPE and EXTENSION enums as well var type: TYPE = TYPE.EPUB, @@ -68,6 +64,54 @@ data class Publication( ) : JSONable, Parcelable { + /** + * Creates a [Publication]'s [positionList]. + * + * The parsers provide an implementation of this interface for each format, but a host app + * might want to use a custom factory to implement, for example, a caching mechanism or use a + * different calculation method. + */ + interface PositionListFactory { + fun create(): List + + /** + * Implementation of a [Parceler] to be used with [@Parcelize] to serialize a + * [PositionListFactory]. + * + * This won't be needed anymore once we use [Fragment] instead of [Activity] in the + * navigator. + */ + object Parceler : kotlinx.android.parcel.Parceler { + + /** + * Boxes a closure into a [PositionListFactory], because closures are serializable. + */ + private class SerializableFactory(private val factory: (() -> List)? = null): PositionListFactory { + override fun create(): List = + factory?.let { it() } ?: emptyList() + } + + override fun create(parcel: Parcel): PositionListFactory? = + try { + (parcel.readSerializable() as? () -> List) + ?.let { SerializableFactory(it) } + } catch (e: Exception) { + Timber.e(e, "Failed to read a PositionListFactory from a Parcel") + null + } + + override fun PositionListFactory?.write(parcel: Parcel, flags: Int) { + try { + // Wrap the [PositionListFactory] in a closure to make it serializable. + parcel.writeSerializable(this?.let { { create() } as Serializable }) + } catch (e: Exception) { + Timber.e(e, "Failed to write a PositionListFactory into a Parcel") + } + } + + } + } + @Parcelize enum class TYPE : Parcelable { EPUB, CBZ, FXL, WEBPUB, AUDIO, DiViNa @@ -94,7 +138,7 @@ data class Publication( */ @IgnoredOnParcel val positionList: List by lazy { - (positionListFactory as? PositionListFactory)?.invoke(this) ?: emptyList() + positionListFactory?.create() ?: emptyList() } /** diff --git a/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt b/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt index 7e18115a..df32d07d 100644 --- a/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt +++ b/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt @@ -25,7 +25,7 @@ class PublicationTest { links: List = listOf(), readingOrder: List = emptyList(), resources: List = emptyList(), - positionListFactory: PositionListFactory = { emptyList() } + positionListFactory: Publication.PositionListFactory? = null ) = Publication( metadata = Metadata( localizedTitle = LocalizedString(title), @@ -35,7 +35,7 @@ class PublicationTest { links = links, readingOrder = readingOrder, resources = resources, - positionListFactory = positionListFactory as Serializable + positionListFactory = positionListFactory ) @Test fun `parse minimal JSON`() { @@ -277,7 +277,10 @@ class PublicationTest { assertEquals( listOf(Locator(href = "locator", type = "")), createPublication( - positionListFactory = { listOf(Locator(href="locator", type = "")) } + positionListFactory = object : Publication.PositionListFactory { + override fun create(): List = + listOf(Locator(href = "locator", type = "")) + } ).positionList ) } @@ -294,11 +297,13 @@ class PublicationTest { ) ), createPublication( - positionListFactory = { listOf( - Locator(href="res1", type = "text/html", title = "Loc A"), - Locator(href="res2", type = "text/html", title = "Loc B"), - Locator(href="res1", type = "text/html", title = "Loc B") - ) } + positionListFactory = object : Publication.PositionListFactory { + override fun create(): List = listOf( + Locator(href="res1", type = "text/html", title = "Loc A"), + Locator(href="res2", type = "text/html", title = "Loc B"), + Locator(href="res1", type = "text/html", title = "Loc B") + ) + } ).positionListByResource ) } From b9322e31c7a83bd5b62ea7ef4a305ad7156f3415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Mon, 10 Feb 2020 13:19:02 +0100 Subject: [PATCH 4/8] Add Locator.copyWithLocations --- .../readium/r2/shared/publication/Locator.kt | 16 ++++++-- .../r2/shared/publication/LocatorTest.kt | 39 +++++++++++++++++-- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/r2-shared/src/main/java/org/readium/r2/shared/publication/Locator.kt b/r2-shared/src/main/java/org/readium/r2/shared/publication/Locator.kt index 37909b97..fcaeece0 100644 --- a/r2-shared/src/main/java/org/readium/r2/shared/publication/Locator.kt +++ b/r2-shared/src/main/java/org/readium/r2/shared/publication/Locator.kt @@ -143,11 +143,21 @@ data class Locator( } /** - * Returns a copy of the [Locator] after setting the [Locations.progression] to [progression]. + * Shortcut to get a copy of the [Locator] with different [Locations] sub-properties. */ - fun withProgression(progression: Double?) = copy( + fun copyWithLocations( + fragments: List = locations.fragments, + progression: Double? = locations.progression, + position: Int? = locations.position, + totalProgression: Double? = locations.totalProgression, + otherLocations: Map = locations.otherLocations + ) = copy( locations = locations.copy( - progression = progression + fragments = fragments, + progression = progression, + position = position, + totalProgression = totalProgression, + otherLocations = otherLocations ) ) diff --git a/r2-shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt b/r2-shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt index 69b1563f..da2a6990 100644 --- a/r2-shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt +++ b/r2-shared/src/test/java/org/readium/r2/shared/publication/LocatorTest.kt @@ -113,18 +113,51 @@ class LocatorTest { ) } - @Test fun `copy a {Locator} with a new {progression}`() { + @Test fun `copy a {Locator} with different {Locations} sub-properties`() { assertEquals( Locator( href = "http://locator", type = "text/html", - locations = Locator.Locations(position = 42, progression = 4.1) + locations = Locator.Locations( + fragments = listOf("p=4", "frag34"), + progression = 0.74, + position = 42, + totalProgression = 0.32, + otherLocations = mapOf("other" to "other-location") + ) ), Locator( href = "http://locator", type = "text/html", locations = Locator.Locations(position = 42, progression = 2.0) - ).withProgression(4.1) + ).copyWithLocations( + fragments = listOf("p=4", "frag34"), + progression = 0.74, + position = 42, + totalProgression = 0.32, + otherLocations = mapOf("other" to "other-location") + ) + ) + } + + @Test fun `copy a {Locator} with reset {Locations} sub-properties`() { + assertEquals( + Locator( + href = "http://locator", + type = "text/html", + locations = Locator.Locations() + ), + Locator( + href = "http://locator", + type = "text/html", + locations = Locator.Locations(position = 42, progression = 2.0) + ).copyWithLocations( + fragments = emptyList(), + progression = null, + position = null, + totalProgression = null, + otherLocations = emptyMap() + ) ) } From de0a904f4dfc68e23f8f9d938c3a29e1afcc8e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Mon, 10 Feb 2020 15:53:40 +0100 Subject: [PATCH 5/8] Add Publication.copyWithPositionListFactory, Link.indexOfFirstWithHref --- .../java/org/readium/r2/shared/publication/Link.kt | 7 +++++++ .../readium/r2/shared/publication/Publication.kt | 9 +++++++++ .../org/readium/r2/shared/publication/LinkTest.kt | 14 ++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/r2-shared/src/main/java/org/readium/r2/shared/publication/Link.kt b/r2-shared/src/main/java/org/readium/r2/shared/publication/Link.kt index 86559b28..6096bbef 100644 --- a/r2-shared/src/main/java/org/readium/r2/shared/publication/Link.kt +++ b/r2-shared/src/main/java/org/readium/r2/shared/publication/Link.kt @@ -147,3 +147,10 @@ data class Link( get() = rels } + +/** + * Returns the first [Link] with the given [href], or [null] if not found. + */ +fun List.indexOfFirstWithHref(href: String): Int? = + indexOfFirst { it.href == href } + .takeUnless { it == -1 } diff --git a/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index d645c1b3..5ad37970 100644 --- a/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -265,6 +265,15 @@ data class Publication( internal fun linksWithRole(role: String): List = otherCollections.firstWithRole(role)?.links ?: emptyList() + /** + * Copy the [Publication] with a different [PositionListFactory]. + * The provided closure will be used to build the [PositionListFactory], with [this] being the + * [Publication]. + */ + fun copyWithPositionListFactory(createFactory: Publication.() -> PositionListFactory): Publication { + return run { copy(positionListFactory = createFactory()) } + } + companion object { /** diff --git a/r2-shared/src/test/java/org/readium/r2/shared/publication/LinkTest.kt b/r2-shared/src/test/java/org/readium/r2/shared/publication/LinkTest.kt index 5667af8a..6e5f51b1 100644 --- a/r2-shared/src/test/java/org/readium/r2/shared/publication/LinkTest.kt +++ b/r2-shared/src/test/java/org/readium/r2/shared/publication/LinkTest.kt @@ -215,4 +215,18 @@ class LinkTest { ) } + @Test + fun `Find the first index of the {Link} with the given {href} in a list of {Link}`() { + assertNull(listOf(Link(href = "href")).indexOfFirstWithHref("foobar")) + + assertEquals( + 1, + listOf( + Link(href = "href1"), + Link(href = "href2"), + Link(href = "href2") // duplicated on purpose + ).indexOfFirstWithHref("href2") + ) + } + } \ No newline at end of file From 419dea5df0721957b25a231525ef504c9e7dc082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Tue, 11 Feb 2020 08:39:30 +0100 Subject: [PATCH 6/8] Fix serialization of PositionListFactory --- .../r2/shared/publication/Publication.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index 5ad37970..09133702 100644 --- a/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -78,23 +78,24 @@ data class Publication( * Implementation of a [Parceler] to be used with [@Parcelize] to serialize a * [PositionListFactory]. * + * Since we can't serialize a factory, we're loading eagerly the [positionList] to be + * serialized. Upon deserialization, the positions will be wrapped in a static factory. + * * This won't be needed anymore once we use [Fragment] instead of [Activity] in the * navigator. */ object Parceler : kotlinx.android.parcel.Parceler { - /** - * Boxes a closure into a [PositionListFactory], because closures are serializable. - */ - private class SerializableFactory(private val factory: (() -> List)? = null): PositionListFactory { - override fun create(): List = - factory?.let { it() } ?: emptyList() + private class StaticPositionListFactory(private val positionList: List): PositionListFactory { + override fun create(): List = positionList } override fun create(parcel: Parcel): PositionListFactory? = try { - (parcel.readSerializable() as? () -> List) - ?.let { SerializableFactory(it) } + mutableListOf() + .apply { parcel.readParcelableList(this, Locator::class.java.classLoader) } + .let { StaticPositionListFactory(it) } + } catch (e: Exception) { Timber.e(e, "Failed to read a PositionListFactory from a Parcel") null @@ -102,8 +103,7 @@ data class Publication( override fun PositionListFactory?.write(parcel: Parcel, flags: Int) { try { - // Wrap the [PositionListFactory] in a closure to make it serializable. - parcel.writeSerializable(this?.let { { create() } as Serializable }) + parcel.writeParcelableList(this?.create(), flags) } catch (e: Exception) { Timber.e(e, "Failed to write a PositionListFactory into a Parcel") } From 83444f78e636fc8860a086d1566be0586bdfe907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Tue, 18 Feb 2020 09:52:00 +0100 Subject: [PATCH 7/8] Don't use writeParcelableList because it's only available on API level 29 --- .../java/org/readium/r2/shared/publication/Publication.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index a43b196e..ff895175 100644 --- a/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -94,7 +94,10 @@ data class Publication( override fun create(parcel: Parcel): PositionListFactory? = try { mutableListOf() - .apply { parcel.readParcelableList(this, Locator::class.java.classLoader) } + .apply { + @Suppress("UNCHECKED_CAST") + parcel.readList(this as MutableList, Locator::class.java.classLoader) + } .let { StaticPositionListFactory(it) } } catch (e: Exception) { @@ -104,7 +107,7 @@ data class Publication( override fun PositionListFactory?.write(parcel: Parcel, flags: Int) { try { - parcel.writeParcelableList(this?.create(), flags) + parcel.writeList(this?.create()) } catch (e: Exception) { Timber.e(e, "Failed to write a PositionListFactory into a Parcel") } From 76e3c659ea36b98477759643cfe03d37c71e14cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mickae=CC=88l=20Menu?= Date: Thu, 27 Feb 2020 10:15:29 +0100 Subject: [PATCH 8/8] Rename `positionList` to `positions` --- .../readium/r2/shared/publication/Locator.kt | 5 +- .../r2/shared/publication/Publication.kt | 24 +++++----- .../r2/shared/publication/html/DomRange.kt | 10 +++- .../PublicationParcelizationTest.kt | 47 ------------------- .../r2/shared/publication/PublicationTest.kt | 20 ++++---- 5 files changed, 34 insertions(+), 72 deletions(-) delete mode 100644 r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationParcelizationTest.kt diff --git a/r2-shared/src/main/java/org/readium/r2/shared/publication/Locator.kt b/r2-shared/src/main/java/org/readium/r2/shared/publication/Locator.kt index fcaeece0..57bfcdeb 100644 --- a/r2-shared/src/main/java/org/readium/r2/shared/publication/Locator.kt +++ b/r2-shared/src/main/java/org/readium/r2/shared/publication/Locator.kt @@ -171,7 +171,10 @@ data class Locator( companion object { - fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Locator? { + fun fromJSON(json: JSONObject?): Locator? = + fromJSON(json, null) + + internal fun fromJSON(json: JSONObject?, warnings: WarningLogger?): Locator? { val href = json?.optNullableString("href") val type = json?.optNullableString("type") if (href == null || type == null) { diff --git a/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt b/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt index e54cded4..b95af519 100644 --- a/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt +++ b/r2-shared/src/main/java/org/readium/r2/shared/publication/Publication.kt @@ -37,7 +37,7 @@ import java.net.URL * * @param type The kind of publication it is ( Epub, Cbz, ... ) * @param version The version of the publication, if the type needs any. - * @param positionListFactory Factory used to build lazily the [positionList]. + * @param positionsFactory Factory used to build lazily the [positions]. */ @Parcelize data class Publication( @@ -50,7 +50,7 @@ data class Publication( val resources: List = emptyList(), val tableOfContents: List = emptyList(), val otherCollections: List = emptyList(), - val positionListFactory: @WriteWith PositionListFactory? = null, + val positionsFactory: @WriteWith PositionListFactory? = null, // FIXME: To be refactored, with the TYPE and EXTENSION enums as well var type: TYPE = TYPE.EPUB, @@ -66,7 +66,7 @@ data class Publication( ) : JSONable, Parcelable { /** - * Creates a [Publication]'s [positionList]. + * Creates a [Publication]'s [positions]. * * The parsers provide an implementation of this interface for each format, but a host app * might want to use a custom factory to implement, for example, a caching mechanism or use a @@ -79,7 +79,7 @@ data class Publication( * Implementation of a [Parceler] to be used with [@Parcelize] to serialize a * [PositionListFactory]. * - * Since we can't serialize a factory, we're loading eagerly the [positionList] to be + * Since we can't serialize a factory, we're loading eagerly the [positions] to be * serialized. Upon deserialization, the positions will be wrapped in a static factory. * * This won't be needed anymore once we use [Fragment] instead of [Activity] in the @@ -87,8 +87,8 @@ data class Publication( */ object Parceler : kotlinx.android.parcel.Parceler { - private class StaticPositionListFactory(private val positionList: List): PositionListFactory { - override fun create(): List = positionList + private class StaticPositionListFactory(private val positions: List): PositionListFactory { + override fun create(): List = positions } override fun create(parcel: Parcel): PositionListFactory? = @@ -141,16 +141,16 @@ data class Publication( * List of all the positions in the publication. */ @IgnoredOnParcel - val positionList: List by lazy { - positionListFactory?.create() ?: emptyList() + val positions: List by lazy { + positionsFactory?.create() ?: emptyList() } /** * List of all the positions in each resource, indexed by their [href]. */ @IgnoredOnParcel - val positionListByResource: Map> by lazy { - positionList.groupBy { it.href } + val positionsByResource: Map> by lazy { + positions.groupBy { it.href } } /** @@ -274,8 +274,8 @@ data class Publication( * The provided closure will be used to build the [PositionListFactory], with [this] being the * [Publication]. */ - fun copyWithPositionListFactory(createFactory: Publication.() -> PositionListFactory): Publication { - return run { copy(positionListFactory = createFactory()) } + fun copyWithPositionsFactory(createFactory: Publication.() -> PositionListFactory): Publication { + return run { copy(positionsFactory = createFactory()) } } companion object { diff --git a/r2-shared/src/main/java/org/readium/r2/shared/publication/html/DomRange.kt b/r2-shared/src/main/java/org/readium/r2/shared/publication/html/DomRange.kt index dd67a39a..24482756 100644 --- a/r2-shared/src/main/java/org/readium/r2/shared/publication/html/DomRange.kt +++ b/r2-shared/src/main/java/org/readium/r2/shared/publication/html/DomRange.kt @@ -76,7 +76,10 @@ data class DomRange( companion object { - fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): Point? { + fun fromJSON(json: JSONObject?): Point? = + fromJSON(json, null) + + internal fun fromJSON(json: JSONObject?, warnings: WarningLogger?): Point? { val cssSelector = json?.optNullableString("cssSelector") val textNodeIndex = json?.optPositiveInt("textNodeIndex") if (cssSelector == null || textNodeIndex == null) { @@ -104,7 +107,10 @@ data class DomRange( companion object { - fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): DomRange? { + fun fromJSON(json: JSONObject?): DomRange? = + fromJSON(json, null) + + internal fun fromJSON(json: JSONObject?, warnings: WarningLogger?): DomRange? { val start = Point.fromJSON(json?.optJSONObject("start")) if (start == null) { warnings?.log(DomRange::class.java, "[start] is required", json) diff --git a/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationParcelizationTest.kt b/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationParcelizationTest.kt deleted file mode 100644 index 05546988..00000000 --- a/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationParcelizationTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Module: r2-shared-kotlin - * Developers: Mickaël Menu - * - * Copyright (c) 2020. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.shared.publication - -import android.content.Intent -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Assert.* -import org.junit.Test -import org.junit.runner.RunWith -import java.io.Serializable - -// FIXME: To be removed once we migrate from Activity to Fragment, then serializing the Publication won't be necessary anymore – see https://github.com/readium/r2-navigator-kotlin/issues/115 -// Commented because running tests with Roboletric is too slow -//@RunWith(AndroidJUnit4::class) -//class PublicationParcelizationTest { -// -// private fun createPublication( -// positionListFactory: PositionListFactory = { emptyList() } -// ) = Publication( -// metadata = Metadata( -// localizedTitle = LocalizedString("Title") -// ), -// positionListFactory = positionListFactory as Serializable -// ) -// -// @Test fun `parcelization of the {positionListFactory}`() { -// var publication = createPublication( -// positionListFactory = { listOf(Locator(href="locator", type = "")) } -// ) -// val intent = Intent() -// intent.putExtra("publication", publication) -// publication = intent.getParcelableExtra("publication") as Publication -// -// assertEquals( -// listOf(Locator(href = "locator", type = "")), -// publication.positionList -// ) -// } -// -//} diff --git a/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt b/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt index 404b7efa..255e91c7 100644 --- a/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt +++ b/r2-shared/src/test/java/org/readium/r2/shared/publication/PublicationTest.kt @@ -25,7 +25,7 @@ class PublicationTest { links: List = listOf(), readingOrder: List = emptyList(), resources: List = emptyList(), - positionListFactory: Publication.PositionListFactory? = null + positionsFactory: Publication.PositionListFactory? = null ) = Publication( metadata = Metadata( localizedTitle = LocalizedString(title), @@ -35,7 +35,7 @@ class PublicationTest { links = links, readingOrder = readingOrder, resources = resources, - positionListFactory = positionListFactory + positionsFactory = positionsFactory ) @Test fun `parse minimal JSON`() { @@ -261,23 +261,23 @@ class PublicationTest { ) } - @Test fun `get the default empty {positionList}`() { - assertEquals(emptyList(), createPublication().positionList) + @Test fun `get the default empty {positions}`() { + assertEquals(emptyList(), createPublication().positions) } - @Test fun `get the {positionList} computed from the {positionListFactory}`() { + @Test fun `get the {positions} computed from the {positionsFactory}`() { assertEquals( listOf(Locator(href = "locator", type = "")), createPublication( - positionListFactory = object : Publication.PositionListFactory { + positionsFactory = object : Publication.PositionListFactory { override fun create(): List = listOf(Locator(href = "locator", type = "")) } - ).positionList + ).positions ) } - @Test fun `get the {positionListByResource} computed from the {positionListFactory}`() { + @Test fun `get the {positionsByResource} computed from the {positionsFactory}`() { assertEquals( mapOf( "res1" to listOf( @@ -289,14 +289,14 @@ class PublicationTest { ) ), createPublication( - positionListFactory = object : Publication.PositionListFactory { + positionsFactory = object : Publication.PositionListFactory { override fun create(): List = listOf( Locator(href="res1", type = "text/html", title = "Loc A"), Locator(href="res2", type = "text/html", title = "Loc B"), Locator(href="res1", type = "text/html", title = "Loc B") ) } - ).positionListByResource + ).positionsByResource ) }