Skip to content
This repository has been archived by the owner on Jul 29, 2022. It is now read-only.

Commit

Permalink
Add Link properties for archive entry metadata (#169)
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-menu authored Sep 17, 2021
1 parent 51603ef commit 3010a60
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 30 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ All notable changes to this project will be documented in this file.

* (*alpha*) A new Publication `SearchService` to search through the resources' content, with a default implementation `StringSearchService`.
* `ContentProtection.Scheme` can be used to identify protection technologies using unique URI identifiers.
* `Link` objects from archive-based publication assets (e.g. an EPUB/ZIP) have additional properties for entry metadata.
```json
"properties" {
"archive": {
"entryLength": 8273,
"isEntryCompressed": true
}
}
```

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.readium.r2.shared.extensions.addPrefix
import org.readium.r2.shared.extensions.tryOr
import org.readium.r2.shared.extensions.tryOrNull
import org.readium.r2.shared.publication.Link
import org.readium.r2.shared.publication.Properties
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.archive.Archive
import org.readium.r2.shared.util.archive.ArchiveFactory
Expand Down Expand Up @@ -67,10 +68,8 @@ class ArchiveFetcher private constructor(private val archive: Archive) : Fetcher
}

override suspend fun link(): Link {
val compressedLength = entry().map { it.compressedLength }.getOrNull()
?: return originalLink

return originalLink.addProperties(mapOf("compressedLength" to compressedLength))
val entry = entry().getOrNull() ?: return originalLink
return originalLink.addProperties(entry.toLinkProperties())
}

override suspend fun read(range: LongRange?): ResourceTry<ByteArray> =
Expand Down Expand Up @@ -98,11 +97,25 @@ class ArchiveFetcher private constructor(private val archive: Archive) : Fetcher
}

private suspend fun Archive.Entry.toLink(): Link {
val link = Link(
return Link(
href = path.addPrefix("/"),
type = MediaType.of(fileExtension = File(path).extension)?.toString()
type = MediaType.of(fileExtension = File(path).extension)?.toString(),
properties = Properties(toLinkProperties())
)
}

private fun Archive.Entry.toLinkProperties(): Map<String, Any> {
val properties = mutableMapOf<String, Any>(
"archive" to mapOf(
"entryLength" to (compressedLength ?: length ?: 0),
"isEntryCompressed" to (compressedLength != null)
)
)

return compressedLength?.let { link.addProperties(mapOf("compressedLength" to it)) }
?: link
compressedLength?.let {
// FIXME: Legacy property, should be removed in 3.0.0
properties["compressedLength"] = it
}

return properties
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2021 Readium Foundation. All rights reserved.
* Use of this source code is governed by the BSD-style license
* available in the top-level LICENSE file of the project.
*/

package org.readium.r2.shared.publication.archive

import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.json.JSONObject
import org.readium.r2.shared.JSONable
import org.readium.r2.shared.extensions.optNullableBoolean
import org.readium.r2.shared.extensions.optNullableLong
import org.readium.r2.shared.extensions.optNullableString
import org.readium.r2.shared.publication.Properties
import org.readium.r2.shared.publication.encryption.Encryption
import org.readium.r2.shared.publication.encryption.encryption
import org.readium.r2.shared.util.logging.WarningLogger
import org.readium.r2.shared.util.logging.log

// Archive Link Properties Extension

/**
* Holds information about how the resource is stored in the publication archive.
*
* @param entryLength The length of the entry stored in the archive. It might be a compressed length
* if the entry is deflated.
* @param isEntryCompressed Indicates whether the entry was compressed before being stored in the
* archive.
*/
@Parcelize
data class ArchiveProperties(
val entryLength: Long,
val isEntryCompressed: Boolean
) : JSONable, Parcelable {

override fun toJSON(): JSONObject = JSONObject().apply {
put("entryLength", entryLength)
put("isEntryCompressed", isEntryCompressed)
}

companion object {
fun fromJSON(json: JSONObject?, warnings: WarningLogger? = null): ArchiveProperties? {
json ?: return null

val entryLength = json.optNullableLong("entryLength")
val isEntryCompressed = json.optNullableBoolean("isEntryCompressed")
if (entryLength == null || isEntryCompressed == null) {
warnings?.log(ArchiveProperties::class.java, "[entryLength] and [isEntryCompressed] are required", json)
return null
}

return ArchiveProperties(entryLength = entryLength, isEntryCompressed = isEntryCompressed)
}

}
}

/**
* Provides information about how the resource is stored in the publication archive.
*/
val Properties.archive: ArchiveProperties?
get() = (this["archive"] as? Map<*, *>)
?.let { ArchiveProperties.fromJSON(JSONObject(it)) }
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

package org.readium.r2.shared.util.archive

import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.readium.r2.shared.extensions.tryOr
Expand Down Expand Up @@ -49,7 +48,10 @@ interface Archive : SuspendingCloseable {
*/
interface Entry : SuspendingCloseable {

/** Absolute path to the entry in the archive. */
/**
* Absolute path to the entry in the archive.
* It MUST start with /.
*/
val path: String

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ package org.readium.r2.shared.fetcher

import android.webkit.MimeTypeMap
import kotlinx.coroutines.runBlocking
import org.json.JSONObject
import org.junit.Test
import org.junit.runner.RunWith
import org.readium.r2.shared.assertJSONEquals
import org.readium.r2.shared.lengthBlocking
import org.readium.r2.shared.linkBlocking
import org.readium.r2.shared.publication.Link
Expand Down Expand Up @@ -46,29 +48,36 @@ class ArchiveFetcherTest {
addExtensionMimeTypMapping("xml", "text/xml")
}

fun createLink(href: String, type: String?, compressedLength: Long? = null) = Link(
href = href,
type = type,
properties =
Properties(
compressedLength
?.let {mapOf("compressedLength" to compressedLength) }
?: mapOf()
fun createLink(href: String, type: String?, entryLength: Long, isCompressed: Boolean): Link {
val props = mutableMapOf<String, Any>(
"archive" to mapOf(
"entryLength" to entryLength,
"isEntryCompressed" to isCompressed
)
)
)
if (isCompressed) {
props["compressedLength"] = entryLength
}

return Link(
href = href,
type = type,
properties = Properties(props)
)
}

assertEquals(
listOf(
createLink("/mimetype", null),
createLink("/EPUB/cover.xhtml" , "application/xhtml+xml", 259L),
createLink("/EPUB/css/epub.css", "text/css", 595L),
createLink("/EPUB/css/nav.css", "text/css", 306L),
createLink("/EPUB/images/cover.png", "image/png", 35809L),
createLink("/EPUB/nav.xhtml", "application/xhtml+xml", 2293L),
createLink("/EPUB/package.opf", null, 773L),
createLink("/EPUB/s04.xhtml", "application/xhtml+xml", 118269L),
createLink("/EPUB/toc.ncx", null, 1697),
createLink("/META-INF/container.xml", "text/xml", 176)
createLink("/mimetype", null, 20, false),
createLink("/EPUB/cover.xhtml" , "application/xhtml+xml", 259L, true),
createLink("/EPUB/css/epub.css", "text/css", 595L, true),
createLink("/EPUB/css/nav.css", "text/css", 306L, true),
createLink("/EPUB/images/cover.png", "image/png", 35809L, true),
createLink("/EPUB/nav.xhtml", "application/xhtml+xml", 2293L, true),
createLink("/EPUB/package.opf", null, 773L, true),
createLink("/EPUB/s04.xhtml", "application/xhtml+xml", 118269L, true),
createLink("/EPUB/toc.ncx", null, 1697, true),
createLink("/META-INF/container.xml", "text/xml", 176, true)
),
runBlocking { fetcher.links() }
)
Expand Down Expand Up @@ -136,14 +145,28 @@ class ArchiveFetcherTest {
assertFailsWith<Resource.Exception.NotFound> { resource.lengthBlocking().getOrThrow() }
}

@Test
fun `Adds compressed length and archive properties to the Link`() = runBlocking {
assertJSONEquals(
JSONObject(mapOf(
"compressedLength" to 595L,
"archive" to mapOf(
"entryLength" to 595L,
"isEntryCompressed" to true
)
)),
fetcher.get(Link(href = "/EPUB/css/epub.css")).link().properties.toJSON()
)
}

@Test
fun `Original link properties are kept`() {
val resource = fetcher.get(Link(href = "/mimetype", properties = Properties(mapOf("other" to "property"))))

assertEquals(
Link(href = "/mimetype", properties = Properties(mapOf(
"other" to "property"
"other" to "property",
"archive" to mapOf("entryLength" to 20L, "isEntryCompressed" to false)
))),
resource.linkBlocking()
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.readium.r2.shared.publication.archive

import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.readium.r2.shared.assertJSONEquals
import org.readium.r2.shared.publication.Properties

class PropertiesTest {

@Test
fun `get no archive`() {
assertNull(Properties().archive)
}

@Test
fun `get full archive`() {
assertEquals(
ArchiveProperties(entryLength = 8273, isEntryCompressed = true),
Properties(mapOf(
"archive" to mapOf(
"entryLength" to 8273,
"isEntryCompressed" to true
)
)).archive
)
}

@Test
fun `get invalid archive`() {
assertNull(
Properties(mapOf(
"archive" to mapOf(
"foo" to "bar"
)
)).archive
)
}

@Test
fun `get incomplete archive`() {
assertNull(
Properties(mapOf(
"archive" to mapOf(
"isEntryCompressed" to true
)
)).archive
)

assertNull(
Properties(mapOf(
"archive" to mapOf(
"entryLength" to 8273
)
)).archive
)
}

@Test
fun `get archive JSON`() {
assertJSONEquals(
JSONObject(mapOf(
"entryLength" to 8273L,
"isEntryCompressed" to true
)),
ArchiveProperties(entryLength = 8273, isEntryCompressed = true).toJSON()
)
}

}

0 comments on commit 3010a60

Please sign in to comment.