Skip to content

Commit cb12770

Browse files
committed
Merge branch 'feature/boundless'
2 parents 410d9aa + c208259 commit cb12770

95 files changed

Lines changed: 2028 additions & 1866 deletions

File tree

Some content is hidden

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

README-CHANGES.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -694,8 +694,10 @@
694694
</c:change>
695695
</c:changes>
696696
</c:release>
697-
<c:release date="2025-07-09T08:12:48+00:00" is-open="true" ticket-system="org.lyrasis.jira" version="1.17.0">
698-
<c:changes/>
697+
<c:release date="2025-07-15T08:09:56+00:00" is-open="true" ticket-system="org.lyrasis.jira" version="1.17.0">
698+
<c:changes>
699+
<c:change date="2025-07-15T08:09:56+00:00" summary="Add support for Boundless DRM."/>
700+
</c:changes>
699701
</c:release>
700702
</c:releases>
701703
<c:ticket-systems>

settings.gradle.kts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ val findawayDRM =
3131
propertyBooleanOptional("org.thepalaceproject.findaway.enabled", false)
3232
val overdriveDRM =
3333
propertyBooleanOptional("org.thepalaceproject.overdrive.enabled", false)
34+
val boundlessDRM =
35+
propertyBooleanOptional("org.thepalaceproject.boundless.enabled", false)
3436

3537
println("DRM: org.thepalaceproject.adobeDRM.enabled : $adobeDRM")
36-
println("DRM: org.thepalaceproject.lcp.enabled : $lcpDRM")
38+
println("DRM: org.thepalaceproject.boundless.enabled : $boundlessDRM")
3739
println("DRM: org.thepalaceproject.findaway.enabled : $findawayDRM")
40+
println("DRM: org.thepalaceproject.lcp.enabled : $lcpDRM")
3841
println("DRM: org.thepalaceproject.overdrive.enabled : $overdriveDRM")
3942

4043
dependencyResolutionManagement {
@@ -98,6 +101,17 @@ dependencyResolutionManagement {
98101
)
99102
}
100103

104+
/*
105+
* Conditionally enable Boundless DRM.
106+
*/
107+
108+
if (boundlessDRM && !s3RepositoryEnabled) {
109+
throw GradleException(
110+
"If the org.thepalaceproject.boundlessDRM.enabled property is set to true, " +
111+
"the org.thepalaceproject.s3.depend property must be set to true."
112+
)
113+
}
114+
101115
/*
102116
* The set of repositories used to resolve library dependencies. The order is significant!
103117
*/

simplified-app-palace/build.gradle.kts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ val findawayDRM =
3131
project.findProperty("org.thepalaceproject.findaway.enabled") == "true"
3232
val overdriveDRM =
3333
project.findProperty("org.thepalaceproject.overdrive.enabled") == "true"
34+
val boundlessDRM =
35+
project.findProperty("org.thepalaceproject.boundless.enabled") == "true"
3436

3537
if (adobeDRM) {
3638
palaceAssetsRequired.setProperty(
@@ -39,7 +41,7 @@ if (adobeDRM) {
3941
)
4042
}
4143

42-
if (adobeDRM || lcpDRM || findawayDRM || overdriveDRM) {
44+
if (adobeDRM || lcpDRM || findawayDRM || overdriveDRM || boundlessDRM) {
4345
palaceAssetsRequired.setProperty(
4446
"assets/secrets.conf",
4547
"221db5c8c1ce1ddbc4f4c1a017f5b63271518d2adf6991010c2831a58b7f88ed",
@@ -380,6 +382,16 @@ dependencies {
380382
implementation(libs.palace.overdrive)
381383
}
382384

385+
/*
386+
* Dependencies conditional upon Boundless DRM support.
387+
*/
388+
389+
if (boundlessDRM) {
390+
implementation(libs.palace.drm.boundless.core)
391+
implementation(libs.palace.drm.boundless.readium)
392+
implementation(libs.palace.drm.boundless.service)
393+
}
394+
383395
/*
384396
* Dependencies needed for Feedbooks JWT handling. Always enabled.
385397
*/
Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package org.nypl.simplified.books.api
22

33
import android.app.Application
4+
import org.nypl.drm.core.BoundlessServiceType
45
import org.nypl.drm.core.ContentProtectionProvider
56
import org.nypl.simplified.lcp.LCPContentProtectionProvider
67
import org.readium.r2.shared.publication.protection.ContentProtection
78
import org.slf4j.LoggerFactory
9+
import java.time.OffsetDateTime
810

911
object BookContentProtections {
1012

@@ -13,40 +15,79 @@ object BookContentProtections {
1315

1416
/*
1517
* Instantiate any content protections that might be needed for DRM...
18+
*
19+
* XXX: This interface needs to be replaced with something else. It's messy and is trying to
20+
* squash multiple different interfaces into a single method.
1621
*/
1722

1823
fun create(
1924
context: Application,
2025
contentProtectionProviders: List<ContentProtectionProvider>,
26+
boundless: BoundlessServiceType?,
27+
format: BookFormat,
2128
drmInfo: BookDRMInformation,
2229
isManualPassphraseEnabled: Boolean = false,
2330
onLCPDialogDismissed: () -> Unit = {}
2431
): List<ContentProtection> {
25-
return contentProtectionProviders.mapNotNull { provider ->
26-
this.logger.debug("instantiating content protection provider {}", provider.javaClass.canonicalName)
27-
28-
/*
29-
* XXX: It's unpleasant to have to special-case like this, but we don't control the
30-
* org.nypl.drm.core.ContentProtectionProvider interface. When LCP is implemented upstream,
31-
* all of those interfaces can be upgraded to properly support passing in credentials.
32-
*/
33-
34-
if (provider is LCPContentProtectionProvider) {
35-
when (drmInfo) {
36-
is BookDRMInformation.LCP -> {
37-
provider.passphrase = drmInfo.hashedPassphrase
38-
provider.isManualPassphraseEnabled = isManualPassphraseEnabled
39-
provider.onLcpDialogDismissed = onLCPDialogDismissed
32+
return try {
33+
when (drmInfo) {
34+
/*
35+
* ACS doesn't require any special handling. Just instantiate the one provider if it exists.
36+
*/
37+
38+
is BookDRMInformation.ACS -> {
39+
contentProtectionProviders.mapNotNull { provider -> provider.create(context) }
40+
}
41+
42+
/*
43+
* Boundless only works for EPUB files, and requires a bit of information up-front.
44+
*/
45+
is BookDRMInformation.Boundless -> {
46+
if (boundless != null) {
47+
if (format is BookFormat.BookFormatEPUB) {
48+
listOf(
49+
boundless.createContentProtection(
50+
epubFile = format.file!!,
51+
licenseFile = drmInfo.license!!,
52+
tempDirectory = context.cacheDir,
53+
inMemorySizeThreshold = 10_000_000UL,
54+
currentTime = OffsetDateTime.now()
55+
)
56+
)
57+
} else {
58+
listOf()
59+
}
60+
} else {
61+
listOf()
4062
}
63+
}
4164

42-
is BookDRMInformation.ACS,
43-
is BookDRMInformation.AXIS,
44-
BookDRMInformation.None -> {
45-
// do nothing
65+
/*
66+
* LCP requires data up front.
67+
*/
68+
is BookDRMInformation.LCP -> {
69+
val lcpProvider =
70+
contentProtectionProviders.filter { provider -> provider is LCPContentProtectionProvider }
71+
.map { provider -> provider as LCPContentProtectionProvider }
72+
.firstOrNull()
73+
74+
if (lcpProvider != null) {
75+
lcpProvider.passphrase = drmInfo.hashedPassphrase
76+
lcpProvider.isManualPassphraseEnabled = isManualPassphraseEnabled
77+
lcpProvider.onLcpDialogDismissed = onLCPDialogDismissed
78+
listOfNotNull(lcpProvider.create(context))
79+
} else {
80+
listOf()
4681
}
4782
}
83+
84+
BookDRMInformation.None -> {
85+
listOf()
86+
}
4887
}
49-
provider.create(context)
88+
} catch (e: Throwable) {
89+
this.logger.error("Failed to handle DRM providers: ", e)
90+
listOf()
5091
}
5192
}
5293
}

simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookDRMInformation.kt

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -81,30 +81,18 @@ sealed class BookDRMInformation : Serializable {
8181
override val kind: BookDRMKind = BookDRMKind.LCP
8282
}
8383

84-
/**
85-
* The AXIS information associated with a book.
86-
*/
87-
88-
data class AXIS(
89-
84+
data class Boundless(
9085
/**
9186
* The license file. This is only present if an attempt has been made to fulfill the book.
9287
*/
9388

94-
val license: File?,
95-
96-
/**
97-
* The file containing the key used to fulfill the book. This is only present
98-
* if an attempt has been made to fulfill the book.
99-
*/
100-
101-
val userKey: File?
89+
val license: File?
10290
) : BookDRMInformation() {
10391
override fun playerCredentials(): PlayerBookCredentialsType {
10492
return PlayerBookCredentialsNone
10593
}
10694

107-
override val kind: BookDRMKind = BookDRMKind.AXIS
95+
override val kind: BookDRMKind = BookDRMKind.BOUNDLESS
10896
}
10997

11098
/**

simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookDRMKind.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ enum class BookDRMKind {
2525
ACS,
2626

2727
/**
28-
* See [BookDRMInformation.AXIS]
28+
* See [BookDRMInformation.Boundless]
2929
*/
3030

31-
AXIS
31+
BOUNDLESS
3232
}

simplified-books-borrowing/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies {
1212
implementation(project(":simplified-books-formats-api"))
1313
implementation(project(":simplified-books-registry-api"))
1414
implementation(project(":simplified-content-api"))
15+
implementation(project(":simplified-links"))
1516
implementation(project(":simplified-opds-core"))
1617
implementation(project(":simplified-presentableerror-api"))
1718
implementation(project(":simplified-profiles-api"))
@@ -44,4 +45,5 @@ dependencies {
4445
implementation(libs.rxjava)
4546
implementation(libs.rxjava2)
4647
implementation(libs.slf4j)
48+
implementation(libs.stduritemplate)
4749
}

simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/BorrowContextType.kt

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import org.joda.time.Instant
55
import org.librarysimplified.http.api.LSHTTPClientType
66
import org.librarysimplified.services.api.ServiceDirectoryType
77
import org.nypl.drm.core.AdobeAdeptExecutorType
8-
import org.nypl.drm.core.AxisNowServiceType
8+
import org.nypl.drm.core.BoundlessServiceType
99
import org.nypl.simplified.accounts.database.api.AccountType
1010
import org.nypl.simplified.books.api.Book
1111
import org.nypl.simplified.books.audio.AudioBookManifestStrategiesType
@@ -16,6 +16,7 @@ import org.nypl.simplified.books.borrowing.subtasks.BorrowSubtaskException.Borro
1616
import org.nypl.simplified.books.borrowing.subtasks.BorrowSubtaskException.BorrowSubtaskFailed
1717
import org.nypl.simplified.books.bundled.api.BundledContentResolverType
1818
import org.nypl.simplified.content.api.ContentResolverType
19+
import org.nypl.simplified.links.Link
1920
import org.nypl.simplified.opds.core.OPDSAcquisitionFeedEntry
2021
import org.nypl.simplified.opds.core.OPDSAcquisitionPath
2122
import org.nypl.simplified.opds.core.OPDSAcquisitionPathElement
@@ -33,7 +34,7 @@ interface BorrowContextType {
3334
val application: Application
3435
val account: AccountType
3536
val adobeExecutor: AdobeAdeptExecutorType?
36-
val axisNowService: AxisNowServiceType?
37+
val boundlessService: BoundlessServiceType?
3738
val audioBookManifestStrategies: AudioBookManifestStrategiesType
3839
val bundledContent: BundledContentResolverType
3940
val clock: () -> Instant
@@ -80,15 +81,15 @@ interface BorrowContextType {
8081
* The current acquisition path element. This will be updated once for each subtask.
8182
*/
8283

83-
fun currentURI(): URI?
84+
fun currentURI(): Link?
8485

8586
/**
8687
* Check that the current URI is non-null. If the current URI is null, log an error
8788
* to the task recorder and throw [BorrowSubtaskFailed].
8889
*/
8990

9091
@Throws(BorrowSubtaskFailed::class)
91-
fun currentURICheck(): URI {
92+
fun currentLinkCheck(): Link {
9293
val uri = this.currentURI()
9394
if (uri == null) {
9495
this.logError("no current URI")
@@ -102,11 +103,31 @@ interface BorrowContextType {
102103
return uri
103104
}
104105

106+
/**
107+
* Perform all of the checks of [currentLinkCheck] and additionally check that the link
108+
* has a valid non-null URI.
109+
*/
110+
111+
@Throws(BorrowSubtaskFailed::class)
112+
fun currentURICheck(): URI {
113+
val link = this.currentLinkCheck()
114+
if (link.hrefURI == null) {
115+
this.logError("no current URI")
116+
this.taskRecorder.currentStepFailed(
117+
message = "A required URI is missing.",
118+
errorCode = BorrowErrorCodes.requiredURIMissing,
119+
extraMessages = listOf()
120+
)
121+
throw BorrowSubtaskFailed()
122+
}
123+
return link.hrefURI!!
124+
}
125+
105126
/**
106127
* The current subtask has received a new URI that can be used by the next subtask.
107128
*/
108129

109-
fun receivedNewURI(uri: URI)
130+
fun receivedNewURI(uri: Link)
110131

111132
/**
112133
* The current acquisition path element. This will be updated once for each subtask.
@@ -238,7 +259,7 @@ interface BorrowContextType {
238259
* Choose a new acquisition path to follow based on the given received entry.
239260
*/
240261

241-
fun chooseNewAcquisitionPath(entry: OPDSAcquisitionFeedEntry): URI
262+
fun chooseNewAcquisitionPath(entry: OPDSAcquisitionFeedEntry): Link
242263

243264
/**
244265
* Information about the current SAML download, if one is in progress.

0 commit comments

Comments
 (0)