From fa30309e57a81029b6dc55eabeead6339f0e10b1 Mon Sep 17 00:00:00 2001
From: Mark Raynsford
Date: Tue, 30 Apr 2024 12:13:54 +0000
Subject: [PATCH 01/11] Initial refactoring for new audiobook player
Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1074
Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1075
Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1076
Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1082
---
.../AudioBookLoadingFragment.kt | 0
.../AudioBookLoadingFragmentListenerType.kt | 0
.../AudioBookLoadingFragmentParameters.kt | 0
.../AudioBookPlayerActivity.kt | 8 +-
org.thepalaceproject.android.platform | 2 +-
.../accounts/api/AccountDescription.java | 2 -
.../api/AccountUnknownProviderException.kt | 2 -
.../AccountUnresolvableProviderException.kt | 2 -
.../accounts/json/AccountPreferencesJSON.kt | 1 -
.../accounts/json/AccountProvidersJSON.kt | 8 +-
...countAuthenticationCredentialsAdobeJSON.kt | 6 +-
.../adobe/extensions/AdobeDRMExtensions.kt | 6 +-
.../adobe/extensions/AdobeDRMServices.java | 8 +-
simplified-app-palace/build.gradle.kts | 11 +-
.../palace/PalaceBuildConfigurationService.kt | 2 +-
.../bookmarks/api/BookmarkAnnotations.kt | 236 +----
.../bookmarks/api/BookmarkAnnotationsJSON.kt | 220 +---
.../simplified/bookmarks/api/BookmarkEvent.kt | 4 +-
.../api/BookmarkServiceUsableType.kt | 42 +-
.../simplified/bookmarks/api/Bookmarks.kt | 18 -
.../bookmarks/api/BookmarksForBook.kt | 37 +
.../simplified/bookmarks/internal/BService.kt | 248 +++--
.../bookmarks/internal/BServiceBookmarks.kt | 89 --
.../internal/BServiceOpCreateBookmark.kt | 12 +-
.../internal/BServiceOpCreateLocalBookmark.kt | 109 +-
.../BServiceOpCreateRemoteBookmark.kt | 60 +-
.../internal/BServiceOpDeleteBookmark.kt | 60 +-
.../internal/BServiceOpLoadBookmarks.kt | 23 +-
.../internal/BServiceOpSyncAllAccounts.kt | 3 +-
.../internal/BServiceOpSyncOneAccount.kt | 218 +---
simplified-books-api/build.gradle.kts | 1 +
.../books/api/BookChapterProgress.kt | 22 -
.../books/api/BookContentProtections.kt | 2 +-
.../nypl/simplified/books/api/BookFormat.kt | 27 +-
.../nypl/simplified/books/api/BookLocation.kt | 50 -
.../simplified/books/api/bookmark/Bookmark.kt | 395 --------
.../books/api/bookmark/BookmarkDigests.kt | 27 +
.../books/api/bookmark/BookmarkID.kt | 5 +-
.../books/api/bookmark/BookmarkJSON.kt | 669 -------------
.../books/api/bookmark/BookmarkMetadata.kt | 43 +
.../books/api/bookmark/SerializedBookmark.kt | 125 +++
.../bookmark/SerializedBookmark20210317.kt | 87 ++
.../bookmark/SerializedBookmark20210828.kt | 87 ++
.../bookmark/SerializedBookmark20240424.kt | 95 ++
.../api/bookmark/SerializedBookmarkLegacy.kt | 111 +++
.../books/api/bookmark/SerializedBookmarks.kt | 238 +++++
.../books/api/bookmark/SerializedLocator.kt | 44 +
.../SerializedLocatorAudioBookTime1.kt | 61 ++
.../SerializedLocatorAudioBookTime2.kt | 43 +
...erializedLocatorHrefProgression20210317.kt | 51 +
.../bookmark/SerializedLocatorLegacyCFI.kt | 47 +
.../api/bookmark/SerializedLocatorPage1.kt | 43 +
.../books/api/bookmark/SerializedLocators.kt | 169 ++++
.../books/api/helper/AudiobookLocationJSON.kt | 102 --
.../books/api/helper/ReaderLocationJSON.kt | 224 -----
simplified-books-audio/build.gradle.kts | 2 +-
.../AbstractAudioBookManifestStrategy.kt | 6 +-
.../audio/AudioBookManifestStrategyType.kt | 2 +-
.../UnpackagedAudioBookManifestStrategy.kt | 2 +-
simplified-books-borrowing/build.gradle.kts | 3 +-
.../books/borrowing/internal/BorrowACSM.kt | 2 +-
.../borrowing/internal/BorrowAudioBook.kt | 2 +-
.../books/controller/BookSyncTask.kt | 1 -
.../simplified/books/controller/Controller.kt | 6 +-
.../books/controller/ProfileFeedTask.kt | 1 -
.../api/BookDRMInformationHandle.kt | 2 +-
.../api/BookDatabaseEntryType.kt | 97 +-
.../DatabaseFormatHandleAudioBook.kt | 122 ++-
.../book_database/DatabaseFormatHandleEPUB.kt | 94 +-
.../book_database/DatabaseFormatHandlePDF.kt | 100 +-
.../book_database/NullDownloadProvider.kt | 9 +-
.../book_registry/BookRegistryReadableType.kt | 3 -
.../time/tracking/TimeTrackingService.kt | 31 +-
.../org/nypl/simplified/feeds/api/Feed.kt | 2 -
.../nypl/simplified/files/FileLocking.java | 1 +
.../json/core/JSONParserUtilities.java | 938 ------------------
.../json/core/JSONParserUtilities.kt | 864 ++++++++++++++++
.../json/core/JSONSerializerUtilities.java | 66 --
.../json/core/JSONSerializerUtilities.kt | 76 ++
.../lcp/LCPContentProtectionProvider.kt | 2 +-
.../main/MainAdobeWarnings.kt | 2 +-
.../main/MainFragmentListenerDelegate.kt | 2 +-
.../AuthenticationDocumentParser.kt | 2 +-
.../core/OPDSAcquisitionFeedEntryParser.java | 38 +-
.../nypl/simplified/opds/core/OPDSAtom.java | 1 -
.../simplified/opds/core/OPDSFeedParser.java | 14 +-
.../opds/core/OPDSSearchParser.java | 8 +-
.../nypl/simplified/opds/core/OPDSXML.java | 1 -
.../opds2/irradia/OPDS2ParsersIrradia.kt | 2 +-
.../nypl/simplified/parser/api/ParseError.kt | 1 -
.../simplified/parser/api/ParseWarning.kt | 1 -
.../profiles/ProfileDescriptionJSON.kt | 37 +-
.../reader/api/ReaderPreferencesJSON.java | 10 +-
.../tenprint/TenPrintGenerator.java | 1 +
simplified-tests/build.gradle.kts | 10 +-
...teAudiobookBookmarkAnnotationsJSONTest.kt} | 71 +-
.../PDFBookmarkAnnotationsJSONTest.kt | 41 +-
.../ReaderBookmarkAnnotationsJSONTest.kt | 80 +-
.../bookmarks/AudiobookBookmarkJSONTest.kt | 107 --
.../tests/bookmarks/ReaderBookmarkJSONTest.kt | 346 -------
.../tests/books/AccountBug0613d7f6.kt | 1 -
.../accounts/AccountsDatabaseContract.kt | 1 -
.../books/accounts/AccountsDatabaseTest.java | 1 -
.../audio/AudioBookManifestStrategyTest.kt | 2 +-
.../books/audio/AudioBookSucceedingParsers.kt | 9 +-
.../BookDatabaseAudioBookContract.kt | 1 -
.../BookDatabaseAudioBookTest.kt | 1 -
.../book_database/BookDatabaseContract.kt | 60 +-
.../book_database/BookDatabaseEPUBContract.kt | 82 +-
.../book_database/BookDatabaseEPUBTest.java | 1 -
.../book_database/BookDatabasePDFContract.kt | 43 +-
.../book_database/BookDatabasePDFTest.java | 1 -
.../books/book_database/BookDatabaseTest.java | 1 -
.../tests/books/bookmarks/BHTTPCallsTest.kt | 9 +-
.../bookmarks/BookmarkServiceContract.kt | 64 +-
.../books/bookmarks/BookmarkServiceTest.kt | 2 +-
.../bookmarks/BookmarksSerializationTest.kt | 320 ++++++
.../tests/books/borrowing/BorrowACSMTest.kt | 1 -
.../books/borrowing/BorrowAudioBookTest.kt | 1 -
.../books/borrowing/BorrowAxisNowTest.kt | 1 -
.../tests/books/borrowing/BorrowCopyTest.kt | 1 -
.../borrowing/BorrowDirectDownloadTest.kt | 1 -
.../books/borrowing/BorrowLimitLoanTest.kt | 1 -
.../books/borrowing/BorrowLoanCreateTest.kt | 1 -
.../books/borrowing/BorrowSAMLDownloadTest.kt | 1 -
.../tests/books/borrowing/BorrowTaskTest.kt | 1 -
.../controller/BookRevokeTaskAdobeDRMTest.kt | 5 -
.../books/controller/BookRevokeTaskTest.kt | 3 -
.../controller/BooksControllerContract.kt | 2 -
.../books/controller/BooksControllerTest.java | 1 -
.../ProfileAccountCreateCustomOPDSTest.kt | 2 -
.../controller/ProfilesControllerContract.kt | 12 +-
.../controller/ProfilesControllerTest.java | 1 -
.../ProfileAccountLoginTaskContract.kt | 2 -
.../ProfileAccountLogoutTaskContract.kt | 2 -
.../profiles/ProfileDescriptionJSONTest.kt | 2 -
.../profiles/ProfilesDatabaseContract.kt | 1 -
.../books/profiles/ProfilesDatabaseTest.java | 1 -
.../simplified/tests/bugs/Simply3635Test.kt | 1 -
.../bookmarks/BookmarkRefreshTokenTest.kt | 1 -
.../borrow/BorrowBookRefreshTokenTest.kt | 1 -
.../lcp/LCPContentProtectionProviderTest.kt | 1 -
.../tests/mocking/MockAccountProviders.kt | 3 -
.../mocking/MockAudioBookManifestStrategy.kt | 4 +-
...kBookDatabaseEntryFormatHandleAudioBook.kt | 21 +-
.../MockBookDatabaseEntryFormatHandleEPUB.kt | 19 +-
.../MockBookDatabaseEntryFormatHandlePDF.kt | 19 +-
.../tests/pdf/PdfViewerProviderTest.kt | 2 +-
.../controller/testBooksRevokeEmptyFeed.xml | 2 +-
.../nypl/simplified/tests/books/groups.xml | 3 +-
.../org/nypl/simplified/tests/books/loans.xml | 10 +-
.../books/revoke-error-empty-feed-revoke.xml | 8 +-
.../tests/opds/acquisition-categories-0.xml | 9 +-
.../tests/opds/acquisition-facets-0.xml | 9 +-
.../tests/opds/acquisition-facets-1.xml | 9 +-
.../tests/opds/acquisition-fiction-0.xml | 4 +-
.../tests/opds/acquisition-groups-0.xml | 5 +-
.../tests/opds/acquisition-paginated-0.xml | 5 +-
.../tests/opds/analytics-20190509.xml | 4 +-
.../simplified/tests/opds/bad-uri-syntax.xml | 9 +-
.../simplified/tests/opds/dpla-test-feed.xml | 5 +-
.../nypl/simplified/tests/opds/empty-0.xml | 9 +-
.../nypl/simplified/tests/opds/entry-0.xml | 10 +-
.../org/nypl/simplified/tests/opds/loans.xml | 3 +-
.../tests/opds/minotaur-20231113.xml | 5 +-
.../simplified/tests/opds/navigation-0.xml | 2 +-
...n-bad-entry-featured-link-without-href.xml | 2 +-
...navigation-bad-entry-link-without-href.xml | 2 +-
.../opds/navigation-bad-entry-no-links.xml | 2 +-
...bad-entry-subsection-link-without-href.xml | 2 +-
.../ui/accounts/AccountCardCreatorFragment.kt | 2 +-
.../ui/accounts/AccountDetailFragment.kt | 6 +-
.../ui/accounts/AccountListFragment.kt | 4 +-
.../accounts/AccountListRegistryFragment.kt | 4 +-
.../accounts/AccountPickerDialogFragment.kt | 2 +-
.../accounts/FilterableAccountListAdapter.kt | 2 +-
.../accounts/saml20/AccountSAML20Fragment.kt | 4 +-
.../accounts/saml20/AccountSAML20ViewModel.kt | 2 +-
.../view_bindings/ViewsForBasicToken.kt | 2 +-
.../view_bindings/ViewsForCOPPAAgeGate.kt | 2 +-
.../accounts/view_bindings/ViewsForSAML20.kt | 2 +-
.../main/res/layout/account_list_item_old.xml | 1 -
.../ui/catalog/AgeGateDialog.kt | 2 +-
.../ui/catalog/CatalogBookDetailFragment.kt | 2 +-
.../ui/catalog/CatalogFeedFragment.kt | 2 +-
.../ui/catalog/CatalogPagedViewHolder.kt | 2 +-
.../src/main/res/layout/book_saml20.xml | 1 -
.../ui/navigation/tabs/BottomNavigators.kt | 8 +-
.../OnboardingDefaultViewModelFactory.kt | 2 +-
.../ui/onboarding/OnboardingFragment.kt | 2 +-
.../res/layout/onboarding_start_screen.xml | 1 -
.../ui/settings/SettingsCustomOPDSFragment.kt | 3 +-
.../ui/settings/SettingsDebugFragment.kt | 2 +-
.../ui/settings/SettingsDebugViewModel.kt | 2 +-
.../SettingsDocumentViewerFragment.kt | 2 +-
.../ui/settings/SettingsMainFragment.kt | 3 +-
.../src/main/res/layout/settings_debug.xml | 8 +-
.../ui/splash/BootViewModel.kt | 2 +-
.../ui/splash/MailtoWebViewClient.kt | 4 +-
.../ui/splash/MigrationReportEmail.kt | 2 +-
.../src/main/res/layout/splash_selection.xml | 2 -
.../org/nypl/simplified/viewer/api/Viewers.kt | 6 +-
simplified-viewer-audiobook/build.gradle.kts | 7 +-
.../viewer/audiobook/AudioBookHelpers.kt | 123 ---
.../audiobook/AudioBookLoadingFragment2.kt | 5 +
.../audiobook/AudioBookPlayerActivity2.kt | 73 ++
.../viewer/audiobook/AudioBookViewer.kt | 7 +-
.../viewer/epub/readium2/Reader2Activity.kt | 4 +-
.../viewer/epub/readium2/Reader2Bookmarks.kt | 121 +--
.../viewer/epub/readium2/ReaderViewerR2.kt | 6 +-
.../viewer/pdf/pdfjs/PdfBookmark.kt | 9 +
.../viewer/pdf/pdfjs/PdfBookmarkKind.kt | 6 +
.../pdfjs/{factory => }/PdfDocumentFactory.kt | 2 +-
.../viewer/pdf/pdfjs/PdfReaderActivity.kt | 198 ++--
.../viewer/pdf/pdfjs/PdfReaderBookmarks.kt | 89 +-
.../pdfjs/{factory => }/PdfReaderDocument.kt | 2 +-
.../viewer/pdf/pdfjs/PdfServer.kt | 1 -
.../viewer/pdf/pdfjs/PdfViewerProvider.kt | 6 +-
.../viewer/spi/ViewerProviderType.kt | 4 +-
.../simplified/webview/WebViewUtilities.kt | 2 +-
220 files changed, 3933 insertions(+), 5070 deletions(-)
rename {simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook => junk}/AudioBookLoadingFragment.kt (100%)
rename {simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook => junk}/AudioBookLoadingFragmentListenerType.kt (100%)
rename {simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook => junk}/AudioBookLoadingFragmentParameters.kt (100%)
rename {simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook => junk}/AudioBookPlayerActivity.kt (99%)
delete mode 100644 simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/Bookmarks.kt
create mode 100644 simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarksForBook.kt
delete mode 100644 simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceBookmarks.kt
delete mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookChapterProgress.kt
delete mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookLocation.kt
delete mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/Bookmark.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkDigests.kt
delete mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkJSON.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkMetadata.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210317.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210828.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20240424.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarkLegacy.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarks.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocator.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime1.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime2.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorHrefProgression20210317.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorLegacyCFI.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorPage1.kt
create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocators.kt
delete mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/AudiobookLocationJSON.kt
delete mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/ReaderLocationJSON.kt
delete mode 100644 simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.java
create mode 100644 simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.kt
delete mode 100644 simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.java
create mode 100644 simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.kt
rename simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/{AudiobookBookmarkAnnotationsJSONTest.kt => ObsoleteAudiobookBookmarkAnnotationsJSONTest.kt} (86%)
delete mode 100644 simplified-tests/src/test/java/org/nypl/simplified/tests/bookmarks/AudiobookBookmarkJSONTest.kt
delete mode 100644 simplified-tests/src/test/java/org/nypl/simplified/tests/bookmarks/ReaderBookmarkJSONTest.kt
create mode 100644 simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarksSerializationTest.kt
delete mode 100644 simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookHelpers.kt
create mode 100644 simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragment2.kt
create mode 100644 simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt
create mode 100644 simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfBookmark.kt
create mode 100644 simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfBookmarkKind.kt
rename simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/{factory => }/PdfDocumentFactory.kt (95%)
rename simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/{factory => }/PdfReaderDocument.kt (97%)
diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragment.kt b/junk/AudioBookLoadingFragment.kt
similarity index 100%
rename from simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragment.kt
rename to junk/AudioBookLoadingFragment.kt
diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragmentListenerType.kt b/junk/AudioBookLoadingFragmentListenerType.kt
similarity index 100%
rename from simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragmentListenerType.kt
rename to junk/AudioBookLoadingFragmentListenerType.kt
diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragmentParameters.kt b/junk/AudioBookLoadingFragmentParameters.kt
similarity index 100%
rename from simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragmentParameters.kt
rename to junk/AudioBookLoadingFragmentParameters.kt
diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity.kt b/junk/AudioBookPlayerActivity.kt
similarity index 99%
rename from simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity.kt
rename to junk/AudioBookPlayerActivity.kt
index 2bb9970df..3f1642d95 100644
--- a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity.kt
+++ b/junk/AudioBookPlayerActivity.kt
@@ -167,7 +167,7 @@ class AudioBookPlayerActivity :
private val reloadingManifest = AtomicBoolean(false)
private val currentBookmarks =
- Collections.synchronizedList(arrayListOf())
+ Collections.synchronizedList(arrayListOf())
@Volatile
private var destroying: Boolean = false
@@ -353,7 +353,7 @@ class AudioBookPlayerActivity :
private fun savePlayerPosition(event: PlayerEventCreateBookmark) {
try {
- val bookmark = Bookmark.AudiobookBookmark.create(
+ val bookmark = Bookmark.ObsoleteAudiobookBookmark.create(
opdsId = this.parameters.opdsEntry.id,
location = PlayerPosition(
title = event.spineElement.position.title,
@@ -690,7 +690,7 @@ class AudioBookPlayerActivity :
try {
val audiobookBookmarks = bookmarks
- .filterIsInstance()
+ .filterIsInstance()
val bookMarkLastReadPosition = audiobookBookmarks.find { bookmark ->
bookmark.kind == BookmarkKind.BookmarkLastReadLocation
@@ -1058,7 +1058,7 @@ class AudioBookPlayerActivity :
bookmark = bookmark,
ignoreRemoteFailures = true
).map { savedBookmark ->
- this.currentBookmarks.add(savedBookmark as Bookmark.AudiobookBookmark)
+ this.currentBookmarks.add(savedBookmark as Bookmark.ObsoleteAudiobookBookmark)
this.showToastMessage(R.string.audio_book_player_bookmark_added)
}.onAnyError {
/* Otherwise, something in the chain failed. */
diff --git a/org.thepalaceproject.android.platform b/org.thepalaceproject.android.platform
index acdff6b8c..b90bb370b 160000
--- a/org.thepalaceproject.android.platform
+++ b/org.thepalaceproject.android.platform
@@ -1 +1 @@
-Subproject commit acdff6b8ce245eae9b08c45da34bd8fe82f1b460
+Subproject commit b90bb370b7045d54ed5143cb2e03c6f2ba57ebae
diff --git a/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountDescription.java b/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountDescription.java
index 2d4efc321..b3956bf2d 100644
--- a/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountDescription.java
+++ b/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountDescription.java
@@ -2,8 +2,6 @@
import com.google.auto.value.AutoValue;
-import java.net.URI;
-
/**
* A description of an account.
*/
diff --git a/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnknownProviderException.kt b/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnknownProviderException.kt
index df643613f..360dd27c6 100644
--- a/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnknownProviderException.kt
+++ b/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnknownProviderException.kt
@@ -1,7 +1,5 @@
package org.nypl.simplified.accounts.api
-import java.lang.Exception
-
/**
* An unrecognized provider was specified when trying to create an account.
*/
diff --git a/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnresolvableProviderException.kt b/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnresolvableProviderException.kt
index aae0c40a4..c19bdc61c 100644
--- a/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnresolvableProviderException.kt
+++ b/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnresolvableProviderException.kt
@@ -1,7 +1,5 @@
package org.nypl.simplified.accounts.api
-import java.lang.Exception
-
/**
* An unresolvable provider was specified when trying to create an account.
*/
diff --git a/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountPreferencesJSON.kt b/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountPreferencesJSON.kt
index 15a638b88..828bf52b2 100644
--- a/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountPreferencesJSON.kt
+++ b/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountPreferencesJSON.kt
@@ -8,7 +8,6 @@ import org.nypl.simplified.accounts.api.AccountPreferences
import org.nypl.simplified.json.core.JSONParseException
import org.nypl.simplified.json.core.JSONParserUtilities
import org.slf4j.LoggerFactory
-import java.lang.Exception
import java.util.UUID
/**
diff --git a/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountProvidersJSON.kt b/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountProvidersJSON.kt
index 0f8ae32d3..e05517c54 100644
--- a/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountProvidersJSON.kt
+++ b/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountProvidersJSON.kt
@@ -17,8 +17,8 @@ import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription
import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.BasicToken
import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.COPPAAgeGate
import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.ANONYMOUS_TYPE
-import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.BASIC_TYPE
import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.BASIC_TOKEN_TYPE
+import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.BASIC_TYPE
import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.COPPA_TYPE
import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.OAUTH_INTERMEDIARY_TYPE
import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.SAML_2_0_TYPE
@@ -486,7 +486,7 @@ object AccountProvidersJSON {
val logoURI =
JSONParserUtilities.getURIOrNull(container, "logo")
val authenticationURI =
- JSONParserUtilities.getURIOrNull(container, "authenticationURI")
+ JSONParserUtilities.getURI(container, "authenticationURI")
BasicToken(
authenticationURI = authenticationURI,
@@ -503,9 +503,9 @@ object AccountProvidersJSON {
COPPA_TYPE -> {
COPPAAgeGate(
greaterEqual13 =
- JSONParserUtilities.getURIOrNull(container, "greaterEqual13"),
+ JSONParserUtilities.getURI(container, "greaterEqual13"),
under13 =
- JSONParserUtilities.getURIOrNull(container, "under13")
+ JSONParserUtilities.getURI(container, "under13")
)
}
else -> {
diff --git a/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/internal/AccountAuthenticationCredentialsAdobeJSON.kt b/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/internal/AccountAuthenticationCredentialsAdobeJSON.kt
index a2632f9d9..313389281 100644
--- a/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/internal/AccountAuthenticationCredentialsAdobeJSON.kt
+++ b/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/internal/AccountAuthenticationCredentialsAdobeJSON.kt
@@ -2,13 +2,13 @@ package org.nypl.simplified.accounts.json.internal
import com.fasterxml.jackson.databind.node.ObjectNode
import com.io7m.jfunctional.Some
+import org.nypl.drm.core.AdobeDeviceID
+import org.nypl.drm.core.AdobeUserID
+import org.nypl.drm.core.AdobeVendorID
import org.nypl.simplified.accounts.api.AccountAuthenticationAdobeClientToken
import org.nypl.simplified.accounts.api.AccountAuthenticationAdobePostActivationCredentials
import org.nypl.simplified.accounts.api.AccountAuthenticationAdobePreActivationCredentials
import org.nypl.simplified.json.core.JSONParserUtilities
-import org.nypl.drm.core.AdobeDeviceID
-import org.nypl.drm.core.AdobeUserID
-import org.nypl.drm.core.AdobeVendorID
object AccountAuthenticationCredentialsAdobeJSON {
diff --git a/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMExtensions.kt b/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMExtensions.kt
index 21f6952d4..07c499774 100644
--- a/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMExtensions.kt
+++ b/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMExtensions.kt
@@ -2,9 +2,6 @@ package org.nypl.simplified.adobe.extensions
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
-import org.nypl.simplified.accounts.api.AccountAuthenticationAdobeClientToken
-import org.nypl.simplified.accounts.api.AccountAuthenticationAdobePostActivationCredentials
-import org.nypl.simplified.files.FileUtilities
import org.nypl.drm.core.AdobeAdeptActivationReceiverType
import org.nypl.drm.core.AdobeAdeptConnectorType
import org.nypl.drm.core.AdobeAdeptDeactivationReceiverType
@@ -15,6 +12,9 @@ import org.nypl.drm.core.AdobeAdeptLoanReturnListenerType
import org.nypl.drm.core.AdobeDeviceID
import org.nypl.drm.core.AdobeUserID
import org.nypl.drm.core.AdobeVendorID
+import org.nypl.simplified.accounts.api.AccountAuthenticationAdobeClientToken
+import org.nypl.simplified.accounts.api.AccountAuthenticationAdobePostActivationCredentials
+import org.nypl.simplified.files.FileUtilities
import java.io.File
import java.util.concurrent.CancellationException
diff --git a/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMServices.java b/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMServices.java
index 1779ec0cd..36fb07fa0 100644
--- a/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMServices.java
+++ b/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMServices.java
@@ -20,10 +20,6 @@
import org.joda.time.Instant;
import org.librarysimplified.adobe.extensions.BuildConfig;
-import org.nypl.simplified.files.DirectoryUtilities;
-import org.nypl.simplified.json.core.JSONParserUtilities;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.nypl.drm.core.AdobeAdeptConnectorFactory;
import org.nypl.drm.core.AdobeAdeptConnectorFactoryType;
import org.nypl.drm.core.AdobeAdeptConnectorParameters;
@@ -38,6 +34,10 @@
import org.nypl.drm.core.AdobeAdeptResourceProviderType;
import org.nypl.drm.core.DRMException;
import org.nypl.drm.core.DRMUnsupportedException;
+import org.nypl.simplified.files.DirectoryUtilities;
+import org.nypl.simplified.json.core.JSONParserUtilities;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
diff --git a/simplified-app-palace/build.gradle.kts b/simplified-app-palace/build.gradle.kts
index e4e701f07..68e1aad82 100644
--- a/simplified-app-palace/build.gradle.kts
+++ b/simplified-app-palace/build.gradle.kts
@@ -356,6 +356,9 @@ dependencies {
*/
if (findawayDRM) {
+ implementation(libs.palace.audiobook.audioengine)
+
+ // Findaway transitive dependencies.
implementation(libs.dagger)
implementation(libs.exoplayer2.core)
implementation(libs.findaway)
@@ -373,12 +376,12 @@ dependencies {
implementation(libs.moshi.kotlin)
implementation(libs.okhttp3)
implementation(libs.okhttp3.logging.interceptor)
- implementation(libs.palace.findaway)
implementation(libs.retrofit2)
implementation(libs.retrofit2.adapter.rxjava)
implementation(libs.retrofit2.converter.gson)
implementation(libs.retrofit2.converter.moshi)
implementation(libs.rxandroid)
+ implementation(libs.rxjava)
implementation(libs.rxrelay)
implementation(libs.sqlbrite)
implementation(libs.stately.common)
@@ -492,6 +495,7 @@ dependencies {
implementation(libs.androidx.viewpager)
implementation(libs.androidx.viewpager2)
implementation(libs.androidx.webkit)
+
implementation(libs.azam.ulidj)
implementation(libs.commons.compress)
implementation(libs.firebase.analytics)
@@ -538,6 +542,7 @@ dependencies {
implementation(libs.javax.inject)
implementation(libs.joda.time)
implementation(libs.jsoup)
+ implementation(libs.kabstand)
implementation(libs.koi.core)
implementation(libs.kotlin.reflect)
implementation(libs.kotlin.stdlib)
@@ -557,7 +562,6 @@ dependencies {
implementation(libs.palace.audiobook.http)
implementation(libs.palace.audiobook.json.canon)
implementation(libs.palace.audiobook.json.web.token)
- implementation(libs.palace.audiobook.lcp)
implementation(libs.palace.audiobook.lcp.license.status)
implementation(libs.palace.audiobook.license.check.api)
implementation(libs.palace.audiobook.license.check.spi)
@@ -569,9 +573,8 @@ dependencies {
implementation(libs.palace.audiobook.manifest.parser.api)
implementation(libs.palace.audiobook.manifest.parser.extension.spi)
implementation(libs.palace.audiobook.manifest.parser.webpub)
- implementation(libs.palace.audiobook.open.access)
+ implementation(libs.palace.audiobook.media3)
implementation(libs.palace.audiobook.parser.api)
- implementation(libs.palace.audiobook.rbdigital)
implementation(libs.palace.audiobook.views)
implementation(libs.palace.drm.core)
implementation(libs.palace.http.api)
diff --git a/simplified-app-palace/src/main/java/org/thepalaceproject/palace/PalaceBuildConfigurationService.kt b/simplified-app-palace/src/main/java/org/thepalaceproject/palace/PalaceBuildConfigurationService.kt
index 8cd0369c4..e1112e703 100644
--- a/simplified-app-palace/src/main/java/org/thepalaceproject/palace/PalaceBuildConfigurationService.kt
+++ b/simplified-app-palace/src/main/java/org/thepalaceproject/palace/PalaceBuildConfigurationService.kt
@@ -1,9 +1,9 @@
package org.thepalaceproject.palace
+import org.librarysimplified.main.BuildConfig
import org.nypl.simplified.buildconfig.api.BuildConfigOAuthScheme
import org.nypl.simplified.buildconfig.api.BuildConfigurationAccountsRegistryURIs
import org.nypl.simplified.buildconfig.api.BuildConfigurationServiceType
-import org.librarysimplified.main.BuildConfig
import java.net.URI
class PalaceBuildConfigurationService : BuildConfigurationServiceType {
diff --git a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotations.kt b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotations.kt
index 6ef22e51c..7da051d8a 100644
--- a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotations.kt
+++ b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotations.kt
@@ -1,12 +1,12 @@
package org.nypl.simplified.bookmarks.api
import com.fasterxml.jackson.databind.ObjectMapper
-import org.joda.time.DateTime
-import org.joda.time.DateTimeZone
import org.joda.time.format.DateTimeFormat
import org.joda.time.format.ISODateTimeFormat
-import org.nypl.simplified.books.api.bookmark.Bookmark
import org.nypl.simplified.books.api.bookmark.BookmarkKind
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark20210828
+import org.nypl.simplified.books.api.bookmark.SerializedLocators
import java.net.URI
data class BookmarkAnnotationSelectorNode(
@@ -22,8 +22,8 @@ data class BookmarkAnnotationTargetNode(
data class BookmarkAnnotationBodyNode(
val timestamp: String,
val device: String,
- val chapterTitle: String?,
- val bookProgress: Float?
+ val chapterTitle: String = "",
+ val bookProgress: Float = 0.0f
)
data class BookmarkAnnotation(
@@ -67,236 +67,58 @@ object BookmarkAnnotations {
private val dateFormatter =
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
- fun fromReaderBookmark(
+ fun fromSerializedBookmark(
objectMapper: ObjectMapper,
- bookmark: Bookmark.ReaderBookmark
+ serializedBookmark: SerializedBookmark
): BookmarkAnnotation {
- /*
- * Check for some values that were likely added by [toBookmark]. Write special values here
- * to ensure that [fromBookmark] is the exact inverse of [toBookmark].
- */
-
- val chapterTitle =
- if (bookmark.chapterTitle == "") {
- null
- } else {
- bookmark.chapterTitle
- }
-
- val bookProgress =
- if (bookmark.bookProgress == 0.0) {
- null
- } else {
- bookmark.bookProgress?.toFloat()
- }
-
val timestamp =
- dateFormatter.print(bookmark.time)
+ this.dateFormatter.print(serializedBookmark.time)
val bodyAnnotation =
BookmarkAnnotationBodyNode(
timestamp = timestamp,
- device = bookmark.deviceID,
- chapterTitle = chapterTitle,
- bookProgress = bookProgress
+ device = serializedBookmark.deviceID,
+ chapterTitle = serializedBookmark.bookChapterTitle,
+ bookProgress = serializedBookmark.bookProgress.toFloat()
)
val locationJSON =
- BookmarkAnnotationsJSON.serializeBookmarkLocation(
- objectMapper = objectMapper,
- bookmark = bookmark
- )
+ serializedBookmark.location.toJSONString(objectMapper)
val target =
BookmarkAnnotationTargetNode(
- bookmark.opdsId,
+ serializedBookmark.opdsId,
BookmarkAnnotationSelectorNode("oa:FragmentSelector", locationJSON)
)
return BookmarkAnnotation(
context = "http://www.w3.org/ns/anno.jsonld",
body = bodyAnnotation,
- id = bookmark.uri?.toString(),
+ id = serializedBookmark.uri?.toString(),
type = "Annotation",
- motivation = bookmark.kind.motivationURI,
+ motivation = serializedBookmark.kind.motivationURI,
target = target
)
}
- fun toReaderBookmark(
+ fun toSerializedBookmark(
objectMapper: ObjectMapper,
annotation: BookmarkAnnotation
- ): Bookmark.ReaderBookmark {
- val locationJSON =
- BookmarkAnnotationsJSON.deserializeReaderLocation(
- objectMapper = objectMapper,
- value = annotation.target.selector.value
- )
-
- val time =
- if (annotation.body.timestamp != null) {
- dateParser.parseDateTime(annotation.body.timestamp)
- } else {
- DateTime.now(DateTimeZone.UTC)
- }
-
- return Bookmark.ReaderBookmark.create(
- opdsId = annotation.target.source,
- location = locationJSON,
- kind = BookmarkKind.ofMotivation(annotation.motivation),
- time = time,
- chapterTitle = annotation.body.chapterTitle ?: "",
- bookProgress = annotation.body.bookProgress?.toDouble(),
- uri = if (annotation.id != null) URI.create(annotation.id) else null,
- deviceID = annotation.body.device
- )
- }
-
- fun fromAudiobookBookmark(
- objectMapper: ObjectMapper,
- bookmark: Bookmark.AudiobookBookmark
- ): BookmarkAnnotation {
- /*
- * Check for some values that were likely added by [toBookmark]. Write special values here
- * to ensure that [fromBookmark] is the exact inverse of [toBookmark].
- */
-
- val timestamp =
- this.dateFormatter.print(bookmark.time)
-
- val bodyAnnotation =
- BookmarkAnnotationBodyNode(
- timestamp = timestamp,
- device = bookmark.deviceID,
- chapterTitle = bookmark.location.title.orEmpty(),
- bookProgress = null
- )
-
- val locationJSON =
- BookmarkAnnotationsJSON.serializeBookmarkLocation(
- objectMapper = objectMapper,
- bookmark = bookmark
- )
-
- val target =
- BookmarkAnnotationTargetNode(
- bookmark.opdsId,
- BookmarkAnnotationSelectorNode("oa:FragmentSelector", locationJSON)
- )
-
- return BookmarkAnnotation(
- context = "http://www.w3.org/ns/anno.jsonld",
- body = bodyAnnotation,
- id = bookmark.uri?.toString(),
- type = "Annotation",
- motivation = bookmark.kind.motivationURI,
- target = target
- )
- }
-
- fun toAudiobookBookmark(
- objectMapper: ObjectMapper,
- annotation: BookmarkAnnotation
- ): Bookmark.AudiobookBookmark {
- val locationJSON =
- BookmarkAnnotationsJSON.deserializeAudiobookLocation(
- objectMapper = objectMapper,
- value = annotation.target.selector.value
- )
-
- val duration =
- BookmarkAnnotationsJSON.deserializeAudiobookDuration(
- objectMapper = objectMapper,
- value = annotation.target.selector.value
- )
-
- val time =
- if (annotation.body.timestamp != null) {
- this.dateParser.parseDateTime(annotation.body.timestamp)
- } else {
- DateTime.now(DateTimeZone.UTC)
- }
-
- return Bookmark.AudiobookBookmark.create(
- opdsId = annotation.target.source,
- location = locationJSON,
- duration = duration,
- kind = BookmarkKind.ofMotivation(annotation.motivation),
- time = time,
- uri = if (annotation.id != null) URI.create(annotation.id) else null,
- deviceID = annotation.body.device
- )
- }
-
- fun fromPdfBookmark(
- objectMapper: ObjectMapper,
- bookmark: Bookmark.PDFBookmark
- ): BookmarkAnnotation {
- /*
- * Check for some values that were likely added by [toBookmark]. Write special values here
- * to ensure that [fromBookmark] is the exact inverse of [toBookmark].
- */
-
- val chapterTitle = null
- val bookProgress = null
-
- val timestamp =
- dateFormatter.print(bookmark.time)
-
- val bodyAnnotation =
- BookmarkAnnotationBodyNode(
- timestamp = timestamp,
- device = bookmark.deviceID,
- chapterTitle = chapterTitle,
- bookProgress = bookProgress
- )
-
- val locationJSON =
- BookmarkAnnotationsJSON.serializeBookmarkLocation(
- objectMapper = objectMapper,
- bookmark = bookmark
- )
-
- val target =
- BookmarkAnnotationTargetNode(
- bookmark.opdsId,
- BookmarkAnnotationSelectorNode("oa:FragmentSelector", locationJSON)
- )
-
- return BookmarkAnnotation(
- context = "http://www.w3.org/ns/anno.jsonld",
- body = bodyAnnotation,
- id = bookmark.uri?.toString(),
- type = "Annotation",
- motivation = bookmark.kind.motivationURI,
- target = target
- )
- }
-
- fun toPdfBookmark(
- objectMapper: ObjectMapper,
- annotation: BookmarkAnnotation
- ): Bookmark.PDFBookmark {
- val locationJSON =
- BookmarkAnnotationsJSON.deserializePdfLocation(
- objectMapper = objectMapper,
- value = annotation.target.selector.value
- )
-
- val time =
- if (annotation.body.timestamp != null) {
- dateParser.parseDateTime(annotation.body.timestamp)
- } else {
- DateTime.now(DateTimeZone.UTC)
- }
-
- return Bookmark.PDFBookmark.create(
+ ): SerializedBookmark {
+ val location =
+ SerializedLocators.parseLocator(objectMapper.readTree(annotation.target.selector.value))
+
+ return SerializedBookmark20210828(
+ deviceID = annotation.body.device,
+ kind = annotation.kind,
+ location = location,
opdsId = annotation.target.source,
- kind = BookmarkKind.ofMotivation(annotation.motivation),
- time = time,
- pageNumber = locationJSON,
+ time = this.dateParser.parseDateTime(annotation.body.timestamp),
uri = if (annotation.id != null) URI.create(annotation.id) else null,
- deviceID = annotation.body.device
+ bookProgress = annotation.body.bookProgress.toDouble(),
+ bookChapterProgress = 0.0,
+ bookChapterTitle = annotation.body.chapterTitle,
+ bookTitle = ""
)
}
}
diff --git a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotationsJSON.kt b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotationsJSON.kt
index 3f34d659d..38a4f8ddf 100644
--- a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotationsJSON.kt
+++ b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotationsJSON.kt
@@ -8,10 +8,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode
import com.io7m.jfunctional.OptionType
import com.io7m.jfunctional.Some
-import org.librarysimplified.audiobook.api.PlayerPosition
-import org.nypl.simplified.books.api.BookChapterProgress
-import org.nypl.simplified.books.api.BookLocation
-import org.nypl.simplified.books.api.bookmark.Bookmark
+import org.nypl.simplified.books.api.bookmark.SerializedLocators
import org.nypl.simplified.json.core.JSONParseException
import org.nypl.simplified.json.core.JSONParserUtilities
@@ -33,22 +30,8 @@ object BookmarkAnnotationsJSON {
*/
try {
- val selectorNode =
- objectMapper.readTree(value)
- val selectorObj =
- JSONParserUtilities.checkObject(null, selectorNode)
-
- when (JSONParserUtilities.getStringOrNull(selectorObj, "@type")) {
- "LocatorAudioBookTime" -> {
- deserializeAudiobookLocation(objectMapper, value)
- }
- "LocatorPage" -> {
- deserializePdfLocation(objectMapper, value)
- }
- else -> {
- deserializeReaderLocation(objectMapper, value)
- }
- }
+ val selectorNode = objectMapper.readTree(value)
+ SerializedLocators.parseLocator(selectorNode)
} catch (e: Exception) {
throw JSONParseException(e)
}
@@ -137,14 +120,16 @@ object BookmarkAnnotationsJSON {
device =
JSONParserUtilities.getString(node, "http://librarysimplified.org/terms/device"),
chapterTitle =
- JSONParserUtilities.getStringOrNull(node, "http://librarysimplified.org/terms/chapter"),
- bookProgress = mapOptionNull(
- JSONParserUtilities.getDoubleOptional(
- node,
- "http://librarysimplified.org/terms/progressWithinBook"
- )
- .map { x -> x.toFloat() }
- )
+ JSONParserUtilities.getStringDefault(
+ node,
+ "http://librarysimplified.org/terms/chapter",
+ ""
+ ),
+ bookProgress = JSONParserUtilities.getDoubleDefault(
+ node,
+ "http://librarysimplified.org/terms/progressWithinBook",
+ 0.0
+ ).toFloat()
)
}
@@ -309,183 +294,4 @@ object BookmarkAnnotationsJSON {
node = JSONParserUtilities.checkObject(null, node),
)
}
-
- @Throws(JSONParseException::class)
- fun serializeBookmarkLocation(
- objectMapper: ObjectMapper,
- bookmark: Bookmark
- ): String {
- objectMapper.configure(SerializationFeature.INDENT_OUTPUT, false)
- objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
- return when (bookmark) {
- is Bookmark.AudiobookBookmark ->
- objectMapper.writeValueAsString(serializeLocationToNode(objectMapper, bookmark))
- is Bookmark.ReaderBookmark ->
- objectMapper.writeValueAsString(
- serializeLocationToNode(
- objectMapper,
- bookmark.location
- )
- )
- is Bookmark.PDFBookmark -> {
- objectMapper.writeValueAsString(serializeIndexToNode(objectMapper, bookmark.pageNumber))
- }
- else ->
- throw IllegalArgumentException("Unsupported bookmark type: $bookmark")
- }
- }
-
- @Throws(JSONParseException::class)
- private fun serializeIndexToNode(
- objectMapper: ObjectMapper,
- pageIndex: Int
- ): ObjectNode {
- val objectNode = objectMapper.createObjectNode()
- objectNode.put("@type", "LocatorPage")
- objectNode.put("page", pageIndex)
- return objectNode
- }
-
- @Throws(JSONParseException::class)
- private fun serializeLocationToNode(
- objectMapper: ObjectMapper,
- location: BookLocation
- ): ObjectNode {
- val objectNode = objectMapper.createObjectNode()
- return when (location) {
- is BookLocation.BookLocationR2 -> {
- objectNode.put("@type", "LocatorHrefProgression")
- objectNode.put("href", location.progress.chapterHref)
- objectNode.put("progressWithinChapter", location.progress.chapterProgress)
- objectNode
- }
- is BookLocation.BookLocationR1 -> {
- objectNode.put("@type", "LocatorLegacyCFI")
- location.idRef?.let {
- objectNode.put("idref", it)
- }
- location.contentCFI?.let {
- objectNode.put("contentCFI", it)
- }
- objectNode.put("progressWithinChapter", location.progress ?: 0.0)
- objectNode
- }
- }
- }
-
- @Throws(JSONParseException::class)
- private fun serializeLocationToNode(
- objectMapper: ObjectMapper,
- bookmark: Bookmark.AudiobookBookmark
- ): ObjectNode {
- val objectNode = objectMapper.createObjectNode()
- objectNode.put("@type", "LocatorAudioBookTime")
- objectNode.put("chapter", bookmark.location.chapter)
- objectNode.put("startOffset", bookmark.location.startOffset)
- objectNode.put(
- "time",
- bookmark.location.startOffset + bookmark.location.currentOffset
- )
- objectNode.put("part", bookmark.location.part)
- objectNode.put("title", bookmark.location.title.orEmpty())
-
- // these fields are required by the iOS app, so we're sending them but since we don't need them
- // in the Android app, there's no need to parsing them back
- objectNode.put("audiobookID", bookmark.opdsId)
- objectNode.put("duration", bookmark.duration)
-
- return objectNode
- }
-
- @Throws(JSONParseException::class)
- fun deserializeAudiobookLocation(
- objectMapper: ObjectMapper,
- value: String
- ): PlayerPosition {
- val node =
- objectMapper.readTree(value)
- val obj =
- JSONParserUtilities.checkObject(null, node)
-
- val startOffset = JSONParserUtilities.getIntegerDefault(obj, "startOffset", 0).toLong()
-
- return PlayerPosition(
- chapter = JSONParserUtilities.getIntegerDefault(obj, "chapter", 0),
- startOffset = startOffset,
- currentOffset = JSONParserUtilities.getInteger(obj, "time").toLong() - startOffset,
- part = JSONParserUtilities.getIntegerDefault(obj, "part", 0),
- title = JSONParserUtilities.getStringOrNull(obj, "title")
- )
- }
-
- @Throws(JSONParseException::class)
- fun deserializeAudiobookDuration(
- objectMapper: ObjectMapper,
- value: String
- ): Long {
- val node =
- objectMapper.readTree(value)
- val obj =
- JSONParserUtilities.checkObject(null, node)
-
- return JSONParserUtilities.getInteger(obj, "duration").toLong()
- }
-
- @Throws(JSONParseException::class)
- fun deserializePdfLocation(
- objectMapper: ObjectMapper,
- value: String
- ): Int {
- val node =
- objectMapper.readTree(value)
- val obj =
- JSONParserUtilities.checkObject(null, node)
-
- return JSONParserUtilities.getInteger(obj, "page")
- }
-
- @Throws(JSONParseException::class)
- fun deserializeReaderLocation(
- objectMapper: ObjectMapper,
- value: String
- ): BookLocation {
- val node =
- objectMapper.readTree(value)
- val obj =
- JSONParserUtilities.checkObject(null, node)
- val type =
- JSONParserUtilities.getStringOrNull(obj, "@type")
-
- return when (type) {
- "LocatorHrefProgression" ->
- deserializeLocationR2(obj)
- "LocatorLegacyCFI" ->
- deserializeLocationLegacyCFI(obj)
- null ->
- deserializeLocationLegacyCFI(obj)
- else ->
- throw JSONParseException("Unsupported locator type: $type")
- }
- }
-
- private fun deserializeLocationLegacyCFI(
- obj: ObjectNode
- ): BookLocation.BookLocationR1 {
- return BookLocation.BookLocationR1(
- progress = JSONParserUtilities.getDoubleDefault(obj, "progressWithinChapter", 0.0),
- contentCFI = JSONParserUtilities.getStringOrNull(obj, "contentCFI"),
- idRef = JSONParserUtilities.getStringOrNull(obj, "idref"),
- )
- }
-
- private fun deserializeLocationR2(
- obj: ObjectNode
- ): BookLocation.BookLocationR2 {
- val progress =
- BookChapterProgress(
- chapterHref = JSONParserUtilities.getString(obj, "href"),
- chapterProgress = JSONParserUtilities.getDouble(obj, "progressWithinChapter")
- )
- return BookLocation.BookLocationR2(progress)
- }
}
diff --git a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkEvent.kt b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkEvent.kt
index 43376e096..ddcf7d49c 100644
--- a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkEvent.kt
+++ b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkEvent.kt
@@ -1,7 +1,7 @@
package org.nypl.simplified.bookmarks.api
import org.nypl.simplified.accounts.api.AccountID
-import org.nypl.simplified.books.api.bookmark.Bookmark
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
/**
* The type of events published by the bookmark controller.
@@ -31,6 +31,6 @@ sealed class BookmarkEvent {
data class BookmarkSaved(
val accountID: AccountID,
- val bookmark: Bookmark
+ val bookmark: SerializedBookmark
) : BookmarkEvent()
}
diff --git a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkServiceUsableType.kt b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkServiceUsableType.kt
index 7e99decc1..162552eb4 100644
--- a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkServiceUsableType.kt
+++ b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkServiceUsableType.kt
@@ -1,10 +1,10 @@
package org.nypl.simplified.bookmarks.api
-import com.google.common.util.concurrent.FluentFuture
import io.reactivex.Observable
import org.nypl.simplified.accounts.api.AccountID
import org.nypl.simplified.books.api.BookID
-import org.nypl.simplified.books.api.bookmark.Bookmark
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
+import java.util.concurrent.CompletableFuture
/**
* The "usable" bookmark service interface. Usable, in this sense, refers to the
@@ -19,21 +19,20 @@ interface BookmarkServiceUsableType {
val bookmarkEvents: Observable
- /**
- * Sync the bookmarks for the given account.
- */
- fun bookmarkSyncAccount(
- accountID: AccountID,
- bookID: BookID
- ): FluentFuture
-
/**
* Sync the bookmarks for the given account, and load bookmarks for the given book.
*/
fun bookmarkSyncAndLoad(
accountID: AccountID,
book: BookID
- ): FluentFuture
+ ): CompletableFuture
+
+ /**
+ * Sync the bookmarks for the given account.
+ */
+ fun bookmarkSyncAccount(
+ accountID: AccountID
+ ): CompletableFuture>
/**
* The user wants their current bookmarks.
@@ -41,9 +40,8 @@ interface BookmarkServiceUsableType {
fun bookmarkLoad(
accountID: AccountID,
- book: BookID,
- lastReadBookmarkServer: Bookmark?
- ): FluentFuture
+ book: BookID
+ ): CompletableFuture
/**
* Create a local bookmark.
@@ -51,8 +49,8 @@ interface BookmarkServiceUsableType {
fun bookmarkCreateLocal(
accountID: AccountID,
- bookmark: Bookmark
- ): FluentFuture
+ bookmark: SerializedBookmark
+ ): CompletableFuture
/**
* Create a remote bookmark.
@@ -60,8 +58,8 @@ interface BookmarkServiceUsableType {
fun bookmarkCreateRemote(
accountID: AccountID,
- bookmark: Bookmark
- ): FluentFuture
+ bookmark: SerializedBookmark
+ ): CompletableFuture
/**
* Create a local bookmark, and then create a remote bookmark if necessary.
@@ -69,9 +67,9 @@ interface BookmarkServiceUsableType {
fun bookmarkCreate(
accountID: AccountID,
- bookmark: Bookmark,
+ bookmark: SerializedBookmark,
ignoreRemoteFailures: Boolean
- ): FluentFuture
+ ): CompletableFuture
/**
* The user has requested that a bookmark be deleted.
@@ -79,7 +77,7 @@ interface BookmarkServiceUsableType {
fun bookmarkDelete(
accountID: AccountID,
- bookmark: Bookmark,
+ bookmark: SerializedBookmark,
ignoreRemoteFailures: Boolean
- ): FluentFuture
+ ): CompletableFuture
}
diff --git a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/Bookmarks.kt b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/Bookmarks.kt
deleted file mode 100644
index ef6d631c2..000000000
--- a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/Bookmarks.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.nypl.simplified.bookmarks.api
-
-import org.nypl.simplified.books.api.bookmark.Bookmark
-import java.io.Serializable
-
-/**
- * A set of bookmarks.
- *
- * Note: The type is {@link Serializable} purely because the Android API requires this
- * in order pass values of this type between activities. We make absolutely no guarantees
- * that serialized values of this class will be compatible with future releases.
- */
-
-data class Bookmarks(
- val lastReadLocal: Bookmark?,
- val lastReadServer: Bookmark?,
- val bookmarks: List
-) : Serializable
diff --git a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarksForBook.kt b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarksForBook.kt
new file mode 100644
index 000000000..bf44ddf88
--- /dev/null
+++ b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarksForBook.kt
@@ -0,0 +1,37 @@
+package org.nypl.simplified.bookmarks.api
+
+import org.nypl.simplified.books.api.BookID
+import org.nypl.simplified.books.api.bookmark.BookmarkKind
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
+import java.io.Serializable
+
+/**
+ * A set of bookmarks for a specific book.
+ *
+ * Note: The type is {@link Serializable} purely because the Android API requires this
+ * in order pass values of this type between activities. We make absolutely no guarantees
+ * that serialized values of this class will be compatible with future releases.
+ */
+
+data class BookmarksForBook(
+ val bookId: BookID,
+ val lastRead: SerializedBookmark?,
+ val bookmarks: List
+) : Serializable {
+ init {
+ check(this.bookmarks.all { bookmark -> bookmark.kind == BookmarkKind.BookmarkExplicit }) {
+ "All bookmarks must be explicit bookmarks."
+ }
+ check(this.bookmarks.all { bookmark -> bookmark.book == this.bookId }) {
+ "All bookmarks must be for book ${this.bookId}."
+ }
+ if (this.lastRead != null) {
+ check(this.lastRead.kind == BookmarkKind.BookmarkLastReadLocation) {
+ "Last-read bookmark must be of a last-read kind"
+ }
+ check(this.lastRead.book == this.bookId) {
+ "All bookmarks must be for book ${this.bookId}."
+ }
+ }
+ }
+}
diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BService.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BService.kt
index ccb03edbe..f6d63f8f4 100644
--- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BService.kt
+++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BService.kt
@@ -1,11 +1,6 @@
package org.nypl.simplified.bookmarks.internal
import com.fasterxml.jackson.databind.ObjectMapper
-import com.google.common.util.concurrent.FluentFuture
-import com.google.common.util.concurrent.Futures
-import com.google.common.util.concurrent.ListenableFuture
-import com.google.common.util.concurrent.ListeningScheduledExecutorService
-import com.google.common.util.concurrent.MoreExecutors
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.subjects.Subject
@@ -14,23 +9,25 @@ import org.nypl.simplified.accounts.api.AccountEventLoginStateChanged
import org.nypl.simplified.accounts.api.AccountID
import org.nypl.simplified.accounts.api.AccountLoginState.AccountLoggedIn
import org.nypl.simplified.accounts.api.AccountLoginState.AccountLoggingIn
-import org.nypl.simplified.accounts.api.AccountLoginState.AccountLoginFailed
import org.nypl.simplified.accounts.api.AccountLoginState.AccountLoggingInWaitingForExternalAuthentication
import org.nypl.simplified.accounts.api.AccountLoginState.AccountLoggingOut
+import org.nypl.simplified.accounts.api.AccountLoginState.AccountLoginFailed
import org.nypl.simplified.accounts.api.AccountLoginState.AccountLogoutFailed
import org.nypl.simplified.accounts.api.AccountLoginState.AccountNotLoggedIn
import org.nypl.simplified.bookmarks.api.BookmarkEvent
import org.nypl.simplified.bookmarks.api.BookmarkHTTPCallsType
-import org.nypl.simplified.bookmarks.api.Bookmarks
import org.nypl.simplified.bookmarks.api.BookmarkServiceType
+import org.nypl.simplified.bookmarks.api.BookmarksForBook
import org.nypl.simplified.books.api.BookID
-import org.nypl.simplified.books.api.bookmark.Bookmark
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
import org.nypl.simplified.profiles.api.ProfileEvent
import org.nypl.simplified.profiles.api.ProfileNoneCurrentException
import org.nypl.simplified.profiles.api.ProfileSelection
import org.nypl.simplified.profiles.controller.api.ProfilesControllerType
import org.slf4j.LoggerFactory
+import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
class BService(
@@ -43,12 +40,10 @@ class BService(
private val disposables =
CompositeDisposable()
- private val executor: ListeningScheduledExecutorService =
- MoreExecutors.listeningDecorator(
- Executors.newScheduledThreadPool(1) { runnable ->
- BServiceThread(this.threads.invoke(runnable))
- }
- )
+ private val executor: ScheduledExecutorService =
+ Executors.newScheduledThreadPool(1) { runnable ->
+ BServiceThread(this.threads.invoke(runnable))
+ }
private val logger =
LoggerFactory.getLogger(BService::class.java)
@@ -69,7 +64,12 @@ class BService(
* Sync bookmarks hourly.
*/
- this.executor.scheduleAtFixedRate({ this.onRegularSyncTimeElapsed() }, 0L, 1L, TimeUnit.HOURS)
+ this.executor.scheduleAtFixedRate(
+ { this.onRegularSyncTimeElapsed() },
+ 0L,
+ 1L,
+ TimeUnit.HOURS
+ )
}
private fun onProfileEvent(event: ProfileEvent) {
@@ -101,13 +101,27 @@ class BService(
}
}
+ private fun submitOp(
+ op: BServiceOp
+ ): CompletableFuture {
+ val f = CompletableFuture()
+ this.executor.execute {
+ try {
+ f.complete(op.runActual())
+ } catch (e: Throwable) {
+ f.completeExceptionally(e)
+ }
+ }
+ return f
+ }
+
/**
* Asynchronously send and receive all bookmarks.
*/
- private fun sync(): ListenableFuture<*> {
+ private fun sync(): CompletableFuture<*> {
return try {
- this.executor.submit(
+ this.submitOp(
BServiceOpSyncAllAccounts(
this.logger,
this.httpCalls,
@@ -116,12 +130,20 @@ class BService(
this.profilesController.profileCurrent()
)
)
- } catch (e: Exception) {
+ } catch (e: Throwable) {
this.logger.error("sync: unable to sync profile: ", e)
- FluentFuture.from(Futures.immediateFailedFuture(e))
+ this.failedFuture(e)
}
}
+ private fun failedFuture(
+ e: Throwable
+ ): CompletableFuture {
+ val f = CompletableFuture()
+ f.completeExceptionally(e)
+ return f
+ }
+
private fun onAccountLoggedIn() {
this.sync()
}
@@ -143,165 +165,137 @@ class BService(
get() = this.bookmarkEventsOut
override fun bookmarkSyncAccount(
- accountID: AccountID,
- bookID: BookID
- ): FluentFuture {
+ accountID: AccountID
+ ): CompletableFuture> {
return try {
- FluentFuture.from(
- this.executor.submit(
- BServiceOpSyncOneAccount(
- this.logger,
- this.httpCalls,
- this.bookmarkEventsOut,
- this.objectMapper,
- this.profilesController.profileCurrent(),
- accountID,
- bookID
- )
+ this.submitOp(
+ BServiceOpSyncOneAccount(
+ this.logger,
+ this.httpCalls,
+ this.bookmarkEventsOut,
+ this.objectMapper,
+ this.profilesController.profileCurrent(),
+ accountID
)
)
- } catch (e: Exception) {
+ } catch (e: Throwable) {
this.logger.error("sync: unable to sync account: ", e)
- FluentFuture.from(Futures.immediateFailedFuture(e))
+ this.failedFuture(e)
}
}
override fun bookmarkSyncAndLoad(
accountID: AccountID,
book: BookID
- ): FluentFuture {
- return this.bookmarkSyncAccount(accountID, book)
- .transformAsync(
- { lastReadServer ->
- this.bookmarkLoad(accountID, book, lastReadServer)
- },
- this.executor
- )
- .catchingAsync(
- Exception::class.java,
- {
- // If sync fails, continue to load the local bookmarks.
- this.bookmarkLoad(accountID, book, null)
- },
- this.executor
- )
+ ): CompletableFuture {
+ return this.bookmarkSyncAccount(accountID)
+ .exceptionally { listOf() }
+ .thenCompose { this.bookmarkLoad(accountID, book) }
}
override fun bookmarkLoad(
accountID: AccountID,
- book: BookID,
- lastReadBookmarkServer: Bookmark?
- ): FluentFuture {
+ book: BookID
+ ): CompletableFuture {
return try {
- FluentFuture.from(
- this.executor.submit(
- BServiceOpLoadBookmarks(
- logger = this.logger,
- accountID = accountID,
- profile = profilesController.profileCurrent(),
- book = book,
- lastReadBookmarkServer = lastReadBookmarkServer
- )
+ this.submitOp(
+ BServiceOpLoadBookmarks(
+ logger = this.logger,
+ accountID = accountID,
+ profile = this.profilesController.profileCurrent(),
+ book = book
)
)
- } catch (e: ProfileNoneCurrentException) {
- this.logger.error("bookmarkLoad: no profile is current: ", e)
- FluentFuture.from(Futures.immediateFailedFuture(e))
+ } catch (e: Throwable) {
+ this.logger.error("bookmarkLoad: ", e)
+ this.failedFuture(e)
}
}
override fun bookmarkCreateLocal(
accountID: AccountID,
- bookmark: Bookmark
- ): FluentFuture {
+ bookmark: SerializedBookmark
+ ): CompletableFuture {
return try {
- FluentFuture.from(
- this.executor.submit(
- BServiceOpCreateLocalBookmark(
- logger = this.logger,
- bookmarkEventsOut = this.bookmarkEventsOut,
- profile = profilesController.profileCurrent(),
- accountID = accountID,
- bookmark = bookmark
- )
+ this.submitOp(
+ BServiceOpCreateLocalBookmark(
+ logger = this.logger,
+ bookmarkEventsOut = this.bookmarkEventsOut,
+ profile = this.profilesController.profileCurrent(),
+ accountID = accountID,
+ bookmark = bookmark
)
)
- } catch (e: ProfileNoneCurrentException) {
- this.logger.error("bookmarkCreateLocal: no profile is current: ", e)
- FluentFuture.from(Futures.immediateFailedFuture(e))
+ } catch (e: Throwable) {
+ this.logger.error("bookmarkCreateLocal: ", e)
+ this.failedFuture(e)
}
}
override fun bookmarkCreateRemote(
accountID: AccountID,
- bookmark: Bookmark
- ): FluentFuture {
+ bookmark: SerializedBookmark
+ ): CompletableFuture {
return try {
- FluentFuture.from(
- this.executor.submit(
- BServiceOpCreateRemoteBookmark(
- logger = this.logger,
- objectMapper = this.objectMapper,
- httpCalls = this.httpCalls,
- profile = profilesController.profileCurrent(),
- accountID = accountID,
- bookmark = bookmark
- )
+ this.submitOp(
+ BServiceOpCreateRemoteBookmark(
+ logger = this.logger,
+ objectMapper = this.objectMapper,
+ httpCalls = this.httpCalls,
+ profile = this.profilesController.profileCurrent(),
+ accountID = accountID,
+ bookmark = bookmark
)
)
- } catch (e: ProfileNoneCurrentException) {
- this.logger.error("bookmarkCreateRemote: no profile is current: ", e)
- FluentFuture.from(Futures.immediateFailedFuture(e))
+ } catch (e: Throwable) {
+ this.logger.error("bookmarkCreateRemote: ", e)
+ this.failedFuture(e)
}
}
override fun bookmarkCreate(
accountID: AccountID,
- bookmark: Bookmark,
+ bookmark: SerializedBookmark,
ignoreRemoteFailures: Boolean
- ): FluentFuture {
+ ): CompletableFuture {
return try {
- FluentFuture.from(
- this.executor.submit(
- BServiceOpCreateBookmark(
- logger = this.logger,
- objectMapper = this.objectMapper,
- httpCalls = this.httpCalls,
- profile = profilesController.profileCurrent(),
- accountID = accountID,
- bookmarkEventsOut = this.bookmarkEventsOut,
- bookmark = bookmark,
- ignoreRemoteFailures = ignoreRemoteFailures
- )
+ this.submitOp(
+ BServiceOpCreateBookmark(
+ logger = this.logger,
+ objectMapper = this.objectMapper,
+ httpCalls = this.httpCalls,
+ profile = this.profilesController.profileCurrent(),
+ accountID = accountID,
+ bookmarkEventsOut = this.bookmarkEventsOut,
+ bookmark = bookmark,
+ ignoreRemoteFailures = ignoreRemoteFailures
)
)
- } catch (e: ProfileNoneCurrentException) {
- this.logger.error("bookmarkCreate: no profile is current: ", e)
- FluentFuture.from(Futures.immediateFailedFuture(e))
+ } catch (e: Throwable) {
+ this.logger.error("bookmarkCreate: ", e)
+ this.failedFuture(e)
}
}
override fun bookmarkDelete(
accountID: AccountID,
- bookmark: Bookmark,
+ bookmark: SerializedBookmark,
ignoreRemoteFailures: Boolean
- ): FluentFuture {
+ ): CompletableFuture {
return try {
- FluentFuture.from(
- this.executor.submit(
- BServiceOpDeleteBookmark(
- logger = this.logger,
- httpCalls = this.httpCalls,
- profile = profilesController.profileCurrent(),
- accountID = accountID,
- bookmark = bookmark,
- ignoreRemoteFailures = ignoreRemoteFailures
- )
+ this.submitOp(
+ BServiceOpDeleteBookmark(
+ logger = this.logger,
+ httpCalls = this.httpCalls,
+ profile = this.profilesController.profileCurrent(),
+ accountID = accountID,
+ bookmark = bookmark,
+ ignoreRemoteFailures = ignoreRemoteFailures
)
)
- } catch (e: ProfileNoneCurrentException) {
- this.logger.error("bookmarkLoad: no profile is current: ", e)
- FluentFuture.from(Futures.immediateFailedFuture(e))
+ } catch (e: Throwable) {
+ this.logger.error("bookmarkLoad: ", e)
+ this.failedFuture(e)
}
}
}
diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceBookmarks.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceBookmarks.kt
deleted file mode 100644
index 4b0c8fcef..000000000
--- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceBookmarks.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package org.nypl.simplified.bookmarks.internal
-
-import org.nypl.simplified.books.api.bookmark.Bookmark
-import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle
-import org.nypl.simplified.profiles.api.ProfileID
-import org.slf4j.Logger
-
-internal object BServiceBookmarks {
-
- /**
- * Normalize the set of bookmarks in the book database. This is necessary because bookmarks
- * do not really have identities and we have to manually deduplicate them if we happen to
- * notice that two bookmarks are the same. Bookmarks have a manually calculated "identity"
- * represented by the [Bookmark.bookmarkId] property, and we can use this to effectively
- * deduplicate bookmarks.
- */
-
- fun normalizeBookmarks(
- logger: Logger,
- profileId: ProfileID,
- handle: BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB,
- bookmark: Bookmark.ReaderBookmark
- ): List {
- val originalBookmarks =
- handle.format.bookmarks
- val bookmarksById =
- originalBookmarks.associateBy { mark -> mark.bookmarkId }
- .toMutableMap()
-
- bookmarksById[bookmark.bookmarkId] = bookmark
-
- logger.debug(
- "[{}]: normalized {} -> {} bookmarks",
- profileId.uuid,
- originalBookmarks.size,
- bookmarksById.size
- )
-
- return bookmarksById.values.toList()
- }
-
- fun normalizeBookmarks(
- logger: Logger,
- profileId: ProfileID,
- handle: BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook,
- bookmark: Bookmark.AudiobookBookmark
- ): List {
- val originalBookmarks =
- handle.format.bookmarks
- val bookmarksById =
- originalBookmarks.associateBy { mark -> mark.bookmarkId }
- .toMutableMap()
-
- bookmarksById[bookmark.bookmarkId] = bookmark
-
- logger.debug(
- "[{}]: normalized {} -> {} bookmarks",
- profileId.uuid,
- originalBookmarks.size,
- bookmarksById.size
- )
-
- return bookmarksById.values.toList()
- }
-
- fun normalizeBookmarks(
- logger: Logger,
- profileId: ProfileID,
- handle: BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandlePDF,
- bookmark: Bookmark.PDFBookmark
- ): List {
- val originalBookmarks =
- handle.format.bookmarks
- val bookmarksById =
- originalBookmarks.associateBy { mark -> mark.bookmarkId }
- .toMutableMap()
-
- bookmarksById[bookmark.bookmarkId] = bookmark
-
- logger.debug(
- "[{}]: normalized {} -> {} bookmarks",
- profileId.uuid,
- originalBookmarks.size,
- bookmarksById.size
- )
-
- return bookmarksById.values.toList()
- }
-}
diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateBookmark.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateBookmark.kt
index 240a844d2..43053159d 100644
--- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateBookmark.kt
+++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateBookmark.kt
@@ -5,7 +5,7 @@ import io.reactivex.subjects.Subject
import org.nypl.simplified.accounts.api.AccountID
import org.nypl.simplified.bookmarks.api.BookmarkEvent
import org.nypl.simplified.bookmarks.api.BookmarkHTTPCallsType
-import org.nypl.simplified.books.api.bookmark.Bookmark
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
import org.nypl.simplified.profiles.api.ProfileReadableType
import org.slf4j.Logger
@@ -22,11 +22,11 @@ internal class BServiceOpCreateBookmark(
private val httpCalls: BookmarkHTTPCallsType,
private val profile: ProfileReadableType,
private val accountID: AccountID,
- private val bookmark: Bookmark,
+ private val bookmark: SerializedBookmark,
private val ignoreRemoteFailures: Boolean
-) : BServiceOp(logger) {
+) : BServiceOp(logger) {
- override fun runActual(): Bookmark {
+ override fun runActual(): SerializedBookmark {
return try {
this.createLocalBookmarkFrom(this.bookmark)
@@ -50,7 +50,9 @@ internal class BServiceOpCreateBookmark(
}
}
- private fun createLocalBookmarkFrom(bookmark: Bookmark): Bookmark {
+ private fun createLocalBookmarkFrom(
+ bookmark: SerializedBookmark
+ ): SerializedBookmark {
return BServiceOpCreateLocalBookmark(
this.logger,
this.bookmarkEventsOut,
diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateLocalBookmark.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateLocalBookmark.kt
index a229182ef..4c2ebe904 100644
--- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateLocalBookmark.kt
+++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateLocalBookmark.kt
@@ -3,10 +3,7 @@ package org.nypl.simplified.bookmarks.internal
import io.reactivex.subjects.Subject
import org.nypl.simplified.accounts.api.AccountID
import org.nypl.simplified.bookmarks.api.BookmarkEvent
-import org.nypl.simplified.books.api.bookmark.Bookmark
-import org.nypl.simplified.books.api.bookmark.BookmarkKind
-import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle
-import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
import org.nypl.simplified.profiles.api.ProfileReadableType
import org.slf4j.Logger
@@ -19,14 +16,14 @@ internal class BServiceOpCreateLocalBookmark(
private val bookmarkEventsOut: Subject,
private val profile: ProfileReadableType,
private val accountID: AccountID,
- private val bookmark: Bookmark
-) : BServiceOp(logger) {
+ private val bookmark: SerializedBookmark
+) : BServiceOp(logger) {
- override fun runActual(): Bookmark {
+ override fun runActual(): SerializedBookmark {
return this.locallySaveBookmark()
}
- private fun locallySaveBookmark(): Bookmark {
+ private fun locallySaveBookmark(): SerializedBookmark {
return try {
this.logger.debug(
"[{}]: locally saving bookmark {}",
@@ -34,98 +31,26 @@ internal class BServiceOpCreateLocalBookmark(
this.bookmark.bookmarkId.value
)
- val account = this.profile.account(this.accountID)
- val books = account.bookDatabase
- val entry = books.entry(this.bookmark.book)
+ val account =
+ this.profile.account(this.accountID)
+ val books =
+ account.bookDatabase
+ val entry =
+ books.entry(this.bookmark.book)
- when (bookmark) {
- is Bookmark.ReaderBookmark -> {
- val handle =
- entry.findFormatHandle(BookDatabaseEntryFormatHandleEPUB::class.java)
- ?: throw this.errorNoFormatHandle()
-
- when (this.bookmark.kind) {
- BookmarkKind.BookmarkLastReadLocation ->
- handle.setLastReadLocation(this.bookmark)
-
- BookmarkKind.BookmarkExplicit ->
- handle.setBookmarks(
- BServiceBookmarks.normalizeBookmarks(
- logger = this.logger,
- profileId = this.profile.id,
- handle = handle,
- bookmark = bookmark
- )
- )
- }
-
- this.publishSavedEvent(this.bookmark)
- }
-
- is Bookmark.PDFBookmark -> {
- val handle =
- entry.findFormatHandle(BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandlePDF::class.java)
- ?: throw this.errorNoFormatHandle()
-
- when (this.bookmark.kind) {
- BookmarkKind.BookmarkLastReadLocation ->
- handle.setLastReadLocation(this.bookmark)
-
- BookmarkKind.BookmarkExplicit -> {
- handle.setBookmarks(
- BServiceBookmarks.normalizeBookmarks(
- logger = this.logger,
- profileId = this.profile.id,
- handle = handle,
- bookmark = bookmark
- )
- )
- }
- }
-
- this.publishSavedEvent(this.bookmark)
- }
-
- is Bookmark.AudiobookBookmark -> {
- val handle =
- entry.findFormatHandle(
- BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook::class.java
- ) ?: throw this.errorNoFormatHandle()
-
- val updatedBookmark = bookmark.copy(
- location = bookmark.location.copy(
- currentOffset = bookmark.location.startOffset + bookmark.location.currentOffset
- )
- )
-
- when (this.bookmark.kind) {
- BookmarkKind.BookmarkLastReadLocation ->
- handle.setLastReadLocation(updatedBookmark)
-
- BookmarkKind.BookmarkExplicit ->
- handle.setBookmarks(
- BServiceBookmarks.normalizeBookmarks(
- logger = this.logger,
- profileId = this.profile.id,
- handle = handle,
- bookmark = updatedBookmark
- )
- )
- }
-
- this.publishSavedEvent(updatedBookmark)
- }
-
- else ->
- throw IllegalStateException("Unsupported bookmark type: $bookmark")
+ for (handle in entry.formatHandles) {
+ handle.addBookmark(this.bookmark)
+ this.publishSavedEvent(this.bookmark)
}
+
+ this.bookmark
} catch (e: Exception) {
this.logger.error("error saving bookmark locally: ", e)
throw e
}
}
- private fun publishSavedEvent(updatedBookmark: Bookmark): Bookmark {
+ private fun publishSavedEvent(updatedBookmark: SerializedBookmark): SerializedBookmark {
this.bookmarkEventsOut.onNext(BookmarkEvent.BookmarkSaved(this.accountID, updatedBookmark))
return updatedBookmark
}
diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateRemoteBookmark.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateRemoteBookmark.kt
index 633b85b43..b07508f87 100644
--- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateRemoteBookmark.kt
+++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateRemoteBookmark.kt
@@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import org.nypl.simplified.accounts.api.AccountID
import org.nypl.simplified.bookmarks.api.BookmarkAnnotations
import org.nypl.simplified.bookmarks.api.BookmarkHTTPCallsType
-import org.nypl.simplified.books.api.bookmark.Bookmark
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
import org.nypl.simplified.profiles.api.ProfileReadableType
import org.slf4j.Logger
@@ -18,14 +18,14 @@ internal class BServiceOpCreateRemoteBookmark(
private val httpCalls: BookmarkHTTPCallsType,
private val profile: ProfileReadableType,
private val accountID: AccountID,
- private val bookmark: Bookmark
-) : BServiceOp(logger) {
+ private val bookmark: SerializedBookmark
+) : BServiceOp(logger) {
- override fun runActual(): Bookmark {
+ override fun runActual(): SerializedBookmark {
return this.remotelySendBookmark()
}
- private fun remotelySendBookmark(): Bookmark {
+ private fun remotelySendBookmark(): SerializedBookmark {
return try {
this.logger.debug(
"[{}]: remote sending bookmark {}",
@@ -44,48 +44,18 @@ internal class BServiceOpCreateRemoteBookmark(
return this.bookmark
}
- val bookmarkAnnotation = when (this.bookmark) {
- is Bookmark.ReaderBookmark -> {
- BookmarkAnnotations.fromReaderBookmark(this.objectMapper, this.bookmark)
- }
- is Bookmark.AudiobookBookmark -> {
- BookmarkAnnotations.fromAudiobookBookmark(this.objectMapper, this.bookmark)
- }
- is Bookmark.PDFBookmark -> {
- BookmarkAnnotations.fromPdfBookmark(this.objectMapper, this.bookmark)
- }
- else -> {
- throw IllegalStateException("Unsupported bookmark type: $bookmark")
- }
- }
+ val bookmarkAnnotation =
+ BookmarkAnnotations.fromSerializedBookmark(this.objectMapper, this.bookmark)
- val bookmarkUri = this.httpCalls.bookmarkAdd(
- account = account,
- annotationsURI = syncInfo.annotationsURI,
- credentials = syncInfo.credentials,
- bookmark = bookmarkAnnotation
- ) ?: throw IllegalStateException("Server HTTP call failed")
+ val bookmarkUri =
+ this.httpCalls.bookmarkAdd(
+ account = account,
+ annotationsURI = syncInfo.annotationsURI,
+ credentials = syncInfo.credentials,
+ bookmark = bookmarkAnnotation
+ ) ?: throw IllegalStateException("Server HTTP call failed")
- when (this.bookmark) {
- is Bookmark.ReaderBookmark -> {
- bookmark.copy(
- uri = bookmarkUri
- )
- }
- is Bookmark.AudiobookBookmark -> {
- bookmark.copy(
- uri = bookmarkUri
- )
- }
- is Bookmark.PDFBookmark -> {
- bookmark.copy(
- uri = bookmarkUri
- )
- }
- else -> {
- throw IllegalStateException("Unsupported bookmark type: $bookmark")
- }
- }
+ return this.bookmark.withURI(bookmarkUri)
} catch (e: Exception) {
this.logger.error("error sending bookmark: ", e)
throw e
diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpDeleteBookmark.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpDeleteBookmark.kt
index ccf71800f..0199cba4f 100644
--- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpDeleteBookmark.kt
+++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpDeleteBookmark.kt
@@ -2,9 +2,8 @@ package org.nypl.simplified.bookmarks.internal
import org.nypl.simplified.accounts.api.AccountID
import org.nypl.simplified.bookmarks.api.BookmarkHTTPCallsType
-import org.nypl.simplified.books.api.bookmark.Bookmark
import org.nypl.simplified.books.api.bookmark.BookmarkKind
-import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
import org.nypl.simplified.profiles.api.ProfileReadableType
import org.slf4j.Logger
@@ -17,7 +16,7 @@ internal class BServiceOpDeleteBookmark(
private val httpCalls: BookmarkHTTPCallsType,
private val profile: ProfileReadableType,
private val accountID: AccountID,
- private val bookmark: Bookmark,
+ private val bookmark: SerializedBookmark,
private val ignoreRemoteFailures: Boolean
) : BServiceOp(logger) {
@@ -92,58 +91,17 @@ internal class BServiceOpDeleteBookmark(
val books = account.bookDatabase
val entry = books.entry(this.bookmark.book)
- when (this.bookmark) {
- is Bookmark.ReaderBookmark -> {
- val handle =
- entry.findFormatHandle(BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB::class.java)
- ?: throw this.errorNoFormatHandle()
-
- when (this.bookmark.kind) {
- BookmarkKind.BookmarkLastReadLocation ->
- handle.setLastReadLocation(null)
- BookmarkKind.BookmarkExplicit ->
- handle.setBookmarks(handle.format.bookmarks.minus(this.bookmark))
- }
- }
- is Bookmark.PDFBookmark -> {
- val handle =
- entry.findFormatHandle(BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandlePDF::class.java)
- ?: throw this.errorNoFormatHandle()
-
- when (this.bookmark.kind) {
- BookmarkKind.BookmarkLastReadLocation ->
- handle.setLastReadLocation(null)
- BookmarkKind.BookmarkExplicit -> {
- handle.setBookmarks(handle.format.bookmarks.minus(this.bookmark))
- }
- }
+ for (handle in entry.formatHandles) {
+ when (this.bookmark.kind) {
+ BookmarkKind.BookmarkLastReadLocation ->
+ handle.setLastReadLocation(null)
+ BookmarkKind.BookmarkExplicit ->
+ handle.deleteBookmark(this.bookmark.bookmarkId)
}
- is Bookmark.AudiobookBookmark -> {
- val handle =
- entry.findFormatHandle(BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook::class.java)
- ?: throw this.errorNoFormatHandle()
-
- when (this.bookmark.kind) {
- BookmarkKind.BookmarkLastReadLocation ->
- handle.setLastReadLocation(null)
- BookmarkKind.BookmarkExplicit ->
- handle.setBookmarks(handle.format.bookmarks.minus(this.bookmark))
- }
- }
- else ->
- throw IllegalStateException("Unsupported bookmark type: ${this.bookmark}")
}
} catch (e: Exception) {
- this.logger.error("[{}]: error deleting bookmark locally: ", this.profile.id.uuid, e)
+ this.logger.error("[{}]: Error deleting bookmark locally: ", this.profile.id.uuid, e)
throw e
}
}
-
- private fun errorNoFormatHandle(): IllegalStateException {
- this.logger.debug(
- "[{}]: unable to delete bookmark; no format handle",
- this.profile.id.uuid
- )
- return IllegalStateException("No format handle")
- }
}
diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpLoadBookmarks.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpLoadBookmarks.kt
index e9571e583..5997e5f5f 100644
--- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpLoadBookmarks.kt
+++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpLoadBookmarks.kt
@@ -1,10 +1,10 @@
package org.nypl.simplified.bookmarks.internal
import org.nypl.simplified.accounts.api.AccountID
-import org.nypl.simplified.bookmarks.api.Bookmarks
+import org.nypl.simplified.bookmarks.api.BookmarksForBook
import org.nypl.simplified.books.api.BookFormat
import org.nypl.simplified.books.api.BookID
-import org.nypl.simplified.books.api.bookmark.Bookmark
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook
import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB
import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandlePDF
@@ -19,11 +19,10 @@ internal class BServiceOpLoadBookmarks(
logger: Logger,
private val profile: ProfileReadableType,
private val accountID: AccountID,
- private val book: BookID,
- private val lastReadBookmarkServer: Bookmark?
-) : BServiceOp(logger) {
+ private val book: BookID
+) : BServiceOp(logger) {
- override fun runActual(): Bookmarks {
+ override fun runActual(): BookmarksForBook {
try {
this.logger.debug("[{}]: loading bookmarks for book {}", this.profile.id.uuid, this.book.brief())
@@ -35,8 +34,8 @@ internal class BServiceOpLoadBookmarks(
?: entry.findFormatHandle(BookDatabaseEntryFormatHandlePDF::class.java)
if (handle != null) {
- val bookmarks: List
- val lastReadLocation: Bookmark?
+ val bookmarks: List
+ val lastReadLocation: SerializedBookmark?
when (handle.format) {
is BookFormat.BookFormatEPUB -> {
@@ -66,9 +65,9 @@ internal class BServiceOpLoadBookmarks(
bookmarks.size
)
- return Bookmarks(
- lastReadLocal = lastReadLocation,
- lastReadServer = lastReadBookmarkServer,
+ return BookmarksForBook(
+ bookId = this.book,
+ lastRead = lastReadLocation,
bookmarks = bookmarks
)
}
@@ -77,6 +76,6 @@ internal class BServiceOpLoadBookmarks(
}
this.logger.debug("[{}]: returning empty bookmarks", this.profile.id.uuid)
- return Bookmarks(null, null, listOf())
+ return BookmarksForBook(this.book, null, listOf())
}
}
diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncAllAccounts.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncAllAccounts.kt
index b3bd8e193..c5fdca6a7 100644
--- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncAllAccounts.kt
+++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncAllAccounts.kt
@@ -29,8 +29,7 @@ internal class BServiceOpSyncAllAccounts(
this.bookmarkEventsOut,
this.objectMapper,
this.profile,
- account,
- bookID = null
+ account
).runActual()
} catch (e: Exception) {
this.logger.debug("failed to sync account {}: ", account.uuid, e)
diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncOneAccount.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncOneAccount.kt
index aad634a0f..657d02382 100644
--- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncOneAccount.kt
+++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncOneAccount.kt
@@ -7,11 +7,8 @@ import org.nypl.simplified.bookmarks.api.BookmarkAnnotation
import org.nypl.simplified.bookmarks.api.BookmarkAnnotations
import org.nypl.simplified.bookmarks.api.BookmarkEvent
import org.nypl.simplified.bookmarks.api.BookmarkHTTPCallsType
-import org.nypl.simplified.bookmarks.api.Bookmarks
-import org.nypl.simplified.books.api.BookID
-import org.nypl.simplified.books.api.bookmark.Bookmark
import org.nypl.simplified.books.api.bookmark.BookmarkKind
-import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB
import org.nypl.simplified.profiles.api.ProfileReadableType
import org.slf4j.Logger
@@ -26,11 +23,10 @@ internal class BServiceOpSyncOneAccount(
private val bookmarkEventsOut: Subject,
private val objectMapper: ObjectMapper,
private val profile: ProfileReadableType,
- private val accountID: AccountID,
- private val bookID: BookID?
-) : BServiceOp(logger) {
+ private val accountID: AccountID
+) : BServiceOp>(logger) {
- override fun runActual(): Bookmark? {
+ override fun runActual(): List {
this.logger.debug(
"[{}]: syncing account {}",
this.profile.id.uuid,
@@ -42,30 +38,29 @@ internal class BServiceOpSyncOneAccount(
if (syncable == null) {
this.logger.error("[{}]: account no longer syncable", this.accountID.uuid)
- return null
+ return listOf()
}
if (!syncable.account.preferences.bookmarkSyncingPermitted) {
this.logger.debug("[{}]: syncing not permitted", this.accountID.uuid)
- return null
+ return listOf()
}
- val received = this.readBookmarksFromServer(syncable, bookID)
- this.sendBookmarksToServer(syncable, received.bookmarks)
+ val received = this.readBookmarksFromServer(syncable)
+ this.sendBookmarksToServer(syncable, received)
this.bookmarkEventsOut.onNext(BookmarkEvent.BookmarkSyncFinished(syncable.account.id))
-
- return received.lastReadServer
+ return received
}
private fun sendBookmarksToServer(
syncable: BSyncableAccount,
- received: List
+ received: List
) {
val localExtras =
this.determineExtraLocalBookmarks(received, syncable)
this.logger.debug(
- "[{}]: we have {} bookmarks the server did not have",
+ "[{}]: We have {} bookmarks the server did not have",
this.accountID.uuid,
localExtras.size
)
@@ -73,25 +68,13 @@ internal class BServiceOpSyncOneAccount(
for (bookmark in localExtras) {
try {
this.logger.debug(
- "[{}]: sending bookmark {}",
+ "[{}]: Sending bookmark {}",
this.accountID.uuid,
bookmark.bookmarkId.value
)
- val bookmarkAnnotation = when (bookmark) {
- is Bookmark.ReaderBookmark -> {
- BookmarkAnnotations.fromReaderBookmark(this.objectMapper, bookmark)
- }
- is Bookmark.AudiobookBookmark -> {
- BookmarkAnnotations.fromAudiobookBookmark(this.objectMapper, bookmark)
- }
- is Bookmark.PDFBookmark -> {
- BookmarkAnnotations.fromPdfBookmark(this.objectMapper, bookmark)
- }
- else -> {
- throw IllegalStateException("Unsupported bookmark type: $bookmark")
- }
- }
+ val bookmarkAnnotation =
+ BookmarkAnnotations.fromSerializedBookmark(this.objectMapper, bookmark)
this.httpCalls.bookmarkAdd(
account = syncable.account,
@@ -100,7 +83,7 @@ internal class BServiceOpSyncOneAccount(
bookmark = bookmarkAnnotation
)
} catch (e: Exception) {
- this.logger.error("[{}]: error sending bookmark: ", this.accountID.uuid, e)
+ this.logger.error("[{}]: Error sending bookmark: ", this.accountID.uuid, e)
}
}
}
@@ -111,9 +94,9 @@ internal class BServiceOpSyncOneAccount(
*/
private fun determineExtraLocalBookmarks(
- received: List,
+ received: List,
syncable: BSyncableAccount
- ): Set {
+ ): Set {
return syncable.account.bookDatabase.books()
.map { id -> syncable.account.bookDatabase.entry(id) }
.mapNotNull { entry -> entry.findFormatHandle(BookDatabaseEntryFormatHandleEPUB::class.java) }
@@ -124,22 +107,20 @@ internal class BServiceOpSyncOneAccount(
}
private fun readBookmarksFromServer(
- syncable: BSyncableAccount,
- bookID: BookID?
- ): Bookmarks {
+ syncable: BSyncableAccount
+ ): List {
this.bookmarkEventsOut.onNext(BookmarkEvent.BookmarkSyncStarted(syncable.account.id))
- val bookmarks: List =
+ val bookmarkAnnotations: List =
try {
- val annotations = this.httpCalls.bookmarksGet(
+ this.httpCalls.bookmarksGet(
account = syncable.account,
annotationsURI = syncable.annotationsURI,
credentials = syncable.credentials
)
- annotations.mapNotNull(this::parseBookmarkOrNull)
} catch (e: Exception) {
this.logger.error(
- "[{}]: could not receive bookmarks for account {}: ",
+ "[{}]: Could not receive bookmarks for account {}: ",
this.profile.id.uuid,
syncable.account.id,
e
@@ -147,21 +128,28 @@ internal class BServiceOpSyncOneAccount(
listOf()
}
- this.logger.debug("[{}]: received {} bookmarks", this.profile.id.uuid, bookmarks.size)
+ this.logger.debug(
+ "[{}]: Received {} bookmarks",
+ this.profile.id.uuid,
+ bookmarkAnnotations.size
+ )
- var serverLastReadBookmark: Bookmark? = null
+ val results = arrayListOf()
- for (bookmark in bookmarks) {
+ for (bookmarkAnnotation in bookmarkAnnotations) {
try {
+ val bookmark =
+ BookmarkAnnotations.toSerializedBookmark(this.objectMapper, bookmarkAnnotation)
+
this.logger.debug(
- "[{}]: received bookmark {}",
+ "[{}]: Received bookmark {}",
this.profile.id.uuid,
bookmark.bookmarkId.value
)
if (!syncable.account.bookDatabase.books().contains(bookmark.book)) {
this.logger.debug(
- "[{}]: we no longer have book {}",
+ "[{}]: We no longer have book {}",
this.profile.id.uuid,
bookmark.book.value()
)
@@ -169,106 +157,17 @@ internal class BServiceOpSyncOneAccount(
}
val entry = syncable.account.bookDatabase.entry(bookmark.book)
-
- when (bookmark) {
- is Bookmark.ReaderBookmark -> {
- val handle = entry.findFormatHandle(BookDatabaseEntryFormatHandleEPUB::class.java)
- if (handle != null) {
- when (bookmark.kind) {
- BookmarkKind.BookmarkLastReadLocation -> {
- // check if it's the last read bookmark for the given book ID
- if (bookID != null && bookmark.book == bookID) {
- serverLastReadBookmark = bookmark
- } else {
- handle.setLastReadLocation(bookmark)
- }
- }
- BookmarkKind.BookmarkExplicit -> {
- handle.setBookmarks(
- BServiceBookmarks.normalizeBookmarks(
- logger = this.logger,
- profileId = this.profile.id,
- handle = handle,
- bookmark = bookmark
- )
- )
- }
- }
-
- this.bookmarkEventsOut.onNext(
- BookmarkEvent.BookmarkSaved(
- syncable.account.id,
- bookmark
- )
- )
- }
- }
- is Bookmark.PDFBookmark -> {
- val handle = entry.findFormatHandle(
- BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandlePDF::class.java
+ for (handle in entry.formatHandles) {
+ handle.addBookmark(bookmark)
+ this.bookmarkEventsOut.onNext(
+ BookmarkEvent.BookmarkSaved(
+ syncable.account.id,
+ bookmark
)
- if (handle != null) {
- when (bookmark.kind) {
- BookmarkKind.BookmarkLastReadLocation -> {
- // check if it's the last read bookmark for the given book ID
- if (bookID != null && bookmark.book == bookID) {
- serverLastReadBookmark = bookmark
- } else {
- handle.setLastReadLocation(bookmark)
- }
- }
- BookmarkKind.BookmarkExplicit -> {
- handle.setBookmarks(
- BServiceBookmarks.normalizeBookmarks(
- logger = this.logger,
- profileId = this.profile.id,
- handle = handle,
- bookmark = bookmark
- )
- )
- }
- }
-
- this.bookmarkEventsOut.onNext(
- BookmarkEvent.BookmarkSaved(
- syncable.account.id,
- bookmark
- )
- )
- }
- }
- is Bookmark.AudiobookBookmark -> {
- val handle =
- entry.findFormatHandle(
- BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook::class.java
- )
- if (handle != null) {
- when (bookmark.kind) {
- BookmarkKind.BookmarkLastReadLocation -> {
- handle.setLastReadLocation(bookmark)
- }
- BookmarkKind.BookmarkExplicit -> {
- handle.setBookmarks(
- BServiceBookmarks.normalizeBookmarks(
- logger = this.logger,
- profileId = this.profile.id,
- handle = handle,
- bookmark = bookmark
- )
- )
- }
- }
- this.bookmarkEventsOut.onNext(
- BookmarkEvent.BookmarkSaved(
- syncable.account.id,
- bookmark
- )
- )
- }
- }
- else ->
- throw IllegalStateException("Unsupported bookmark type: $bookmark")
+ )
}
+
+ results.add(bookmark)
} catch (e: Exception) {
this.logger.error(
"[{}]: could not store bookmark for account {}: ",
@@ -279,35 +178,6 @@ internal class BServiceOpSyncOneAccount(
}
}
- return Bookmarks(
- lastReadLocal = null,
- lastReadServer = serverLastReadBookmark,
- bookmarks = bookmarks
- )
- }
-
- private fun parseBookmarkOrNull(
- annotation: BookmarkAnnotation
- ): Bookmark? {
- return try {
- val bookmark = BookmarkAnnotations.toAudiobookBookmark(this.objectMapper, annotation)
- this.logger.debug("Audiobook bookmark successfully parsed")
- bookmark
- } catch (e: Exception) {
- try {
- val bookmark = BookmarkAnnotations.toReaderBookmark(this.objectMapper, annotation)
- this.logger.debug("Reader bookmark successfully parsed")
- bookmark
- } catch (e: Exception) {
- try {
- val bookmark = BookmarkAnnotations.toPdfBookmark(this.objectMapper, annotation)
- this.logger.debug("PDF bookmark successfully parsed")
- bookmark
- } catch (e: Exception) {
- this.logger.error("unable to parse bookmark: ", e)
- null
- }
- }
- }
+ return results.toList()
}
}
diff --git a/simplified-books-api/build.gradle.kts b/simplified-books-api/build.gradle.kts
index f20d9a10d..4bb204683 100644
--- a/simplified-books-api/build.gradle.kts
+++ b/simplified-books-api/build.gradle.kts
@@ -15,6 +15,7 @@ dependencies {
implementation(libs.kotlin.reflect)
implementation(libs.kotlin.stdlib)
implementation(libs.palace.audiobook.api)
+ implementation(libs.palace.audiobook.manifest.api)
implementation(libs.palace.drm.core)
implementation(libs.palace.readium2.api)
implementation(libs.r2.shared)
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookChapterProgress.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookChapterProgress.kt
deleted file mode 100644
index 96dec87a0..000000000
--- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookChapterProgress.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.nypl.simplified.books.api
-
-import java.io.Serializable
-
-/**
- * Progress through a specific chapter.
- */
-
-data class BookChapterProgress(
-
- /**
- * The href of the chapter.
- */
-
- val chapterHref: String,
-
- /**
- * The progress through the chapter.
- */
-
- val chapterProgress: Double
-) : Serializable
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookContentProtections.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookContentProtections.kt
index 5138b0e1f..44cc400ea 100644
--- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookContentProtections.kt
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookContentProtections.kt
@@ -1,10 +1,10 @@
package org.nypl.simplified.books.api
import android.app.Application
+import org.nypl.drm.core.ContentProtectionProvider
import org.nypl.simplified.lcp.LCPContentProtectionProvider
import org.readium.r2.shared.publication.protection.ContentProtection
import org.slf4j.LoggerFactory
-import org.nypl.drm.core.ContentProtectionProvider
object BookContentProtections {
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookFormat.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookFormat.kt
index 25eae2d1f..b94a851f0 100644
--- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookFormat.kt
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookFormat.kt
@@ -1,7 +1,7 @@
package org.nypl.simplified.books.api
import one.irradia.mime.api.MIMEType
-import org.nypl.simplified.books.api.bookmark.Bookmark
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
import java.io.File
import java.net.URI
@@ -30,6 +30,18 @@ sealed class BookFormat {
abstract val isDownloaded: Boolean
+ /**
+ * The last read location of the book, if any.
+ */
+
+ abstract val lastReadLocation: SerializedBookmark?
+
+ /**
+ * The list of bookmarks.
+ */
+
+ abstract val bookmarks: List
+
/**
* An EPUB format.
*/
@@ -47,13 +59,13 @@ sealed class BookFormat {
* The last read location of the book, if any.
*/
- val lastReadLocation: Bookmark.ReaderBookmark?,
+ override val lastReadLocation: SerializedBookmark?,
/**
* The list of bookmarks.
*/
- val bookmarks: List,
+ override val bookmarks: List,
override val contentType: MIMEType
) : BookFormat() {
@@ -105,12 +117,12 @@ sealed class BookFormat {
/**
* The last read location of the audiobook, if any.
*/
- val lastReadLocation: Bookmark.AudiobookBookmark?,
+ override val lastReadLocation: SerializedBookmark?,
/**
* The list of bookmarks.
*/
- val bookmarks: List,
+ override val bookmarks: List,
override val contentType: MIMEType
) : BookFormat() {
@@ -134,12 +146,13 @@ sealed class BookFormat {
* The last read location of the PDF book, if any.
*/
- val lastReadLocation: Bookmark.PDFBookmark?,
+ override val lastReadLocation: SerializedBookmark?,
/**
* The list of bookmarks.
*/
- val bookmarks: List,
+
+ override val bookmarks: List,
/**
* The PDF file on disk, if one has been downloaded.
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookLocation.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookLocation.kt
deleted file mode 100644
index 4a5a98d36..000000000
--- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookLocation.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package org.nypl.simplified.books.api
-
-import java.io.Serializable
-
-/**
- * The current page. A specific location in an EPUB is identified by a chapter index, or an
- * *idref* and a *content CFI*. In some cases, the *content CFI*
- * may not be present.
- *
- * Note: The type is [Serializable] purely because the Android API requires this
- * in order pass values of this type between activities. We make absolutely no guarantees
- * that serialized values of this class will be compatible with future releases.
- */
-
-sealed class BookLocation : Serializable {
-
- /**
- * R2-specific book locations.
- */
-
- data class BookLocationR2(
- val progress: BookChapterProgress
- ) : BookLocation()
-
- /**
- * R1-specific book locations.
- */
-
- @Deprecated("Use R2")
- data class BookLocationR1(
-
- /**
- * The chapter progress.
- */
-
- val progress: Double?,
-
- /**
- * @return The content CFI, if any
- */
-
- val contentCFI: String?,
-
- /**
- * @return The IDRef
- */
-
- val idRef: String?
- ) : BookLocation()
-}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/Bookmark.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/Bookmark.kt
deleted file mode 100644
index ae7e375b8..000000000
--- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/Bookmark.kt
+++ /dev/null
@@ -1,395 +0,0 @@
-package org.nypl.simplified.books.api.bookmark
-
-import org.joda.time.DateTime
-import org.joda.time.DateTimeZone
-import org.librarysimplified.audiobook.api.PlayerPosition
-import org.nypl.simplified.books.api.BookID
-import org.nypl.simplified.books.api.BookIDs
-import org.nypl.simplified.books.api.BookLocation
-import java.io.Serializable
-import java.net.URI
-import java.nio.charset.Charset
-import java.security.MessageDigest
-import java.security.NoSuchAlgorithmException
-
-/**
- * The saved data for a bookmark.
- */
-
-sealed class Bookmark {
-
- /**
- * @return The identifier of the book taken from the OPDS entry that provided it.
- */
-
- abstract val opdsId: String
-
- /**
- * @return The kind of bookmark.
- */
-
- abstract val kind: BookmarkKind
-
- /**
- * @return The time the bookmark was created.
- */
-
- abstract val time: DateTime
-
- /**
- * @return The identifier of the device that created the bookmark, if one is available.
- */
-
- abstract val deviceID: String
-
- /**
- * @return The URI of this bookmark, if the bookmark exists on a remote server.
- */
-
- abstract val uri: URI?
-
- /**
- * The ID of the book to which the bookmark belongs.
- */
-
- abstract val book: BookID
-
- /**
- * The unique ID of the bookmark.
- */
-
- abstract val bookmarkId: BookmarkID
-
- /**
- * Convenience function to convert a bookmark to a last-read-location kind.
- */
- abstract fun toLastReadLocation(): Bookmark
-
- /**
- * Convenience function to convert a bookmark to an explicit kind.
- */
-
- abstract fun toExplicit(): Bookmark
-
- /**
- * Class for bookmarks of reader type.
- *
- * Note: The type is {@link Serializable} purely because the Android API requires this
- * in order pass values of this type between activities. We make absolutely no guarantees
- * that serialized values of this class will be compatible with future releases.
- */
-
- data class ReaderBookmark(
- override val opdsId: String,
- override val time: DateTime,
- override val deviceID: String,
- override val kind: BookmarkKind,
- override val uri: URI?,
-
- /**
- * The title of the chapter.
- */
-
- val chapterTitle: String,
-
- /**
- * The location of the bookmark.
- */
-
- val location: BookLocation,
-
- /**
- * An estimate of the current book progress, in the range [0, 1]
- */
-
- @Deprecated("Use progress information from the BookLocation")
- val bookProgress: Double?
-
- ) : Bookmark(), Serializable {
-
- override val book: BookID = BookIDs.newFromText(this.opdsId)
-
- override val bookmarkId: BookmarkID = createBookmarkID(this.book, this.location, this.kind)
-
- override fun toLastReadLocation(): Bookmark {
- return this.copy(kind = BookmarkKind.BookmarkLastReadLocation)
- }
-
- override fun toExplicit(): Bookmark {
- return this.copy(kind = BookmarkKind.BookmarkExplicit)
- }
-
- init {
- check(this.time.zone == DateTimeZone.UTC) {
- "Bookmark time zones must be UTC"
- }
- }
-
- /**
- * An estimate of the current chapter progress, in the range [0, 1]
- */
-
- val chapterProgress: Double =
- when (this.location) {
- is BookLocation.BookLocationR2 -> this.location.progress.chapterProgress
- is BookLocation.BookLocationR1 -> this.location.progress ?: 0.0
- }
-
- private fun createBookmarkID(
- book: BookID,
- location: BookLocation,
- kind: BookmarkKind
- ): BookmarkID {
- try {
- val messageDigest = MessageDigest.getInstance("SHA-256")
- val utf8 = Charset.forName("UTF-8")
- messageDigest.update(book.value().toByteArray(utf8))
-
- when (location) {
- is BookLocation.BookLocationR2 -> {
- val chapterProgress = location.progress
- messageDigest.update(chapterProgress.chapterHref.toByteArray(utf8))
- val truncatedProgress = String.format("%.6f", chapterProgress.chapterProgress)
- messageDigest.update(truncatedProgress.toByteArray(utf8))
- }
- is BookLocation.BookLocationR1 -> {
- val cfi = location.contentCFI
- if (cfi != null) {
- messageDigest.update(cfi.toByteArray(utf8))
- }
- val idRef = location.idRef
- if (idRef != null) {
- messageDigest.update(idRef.toByteArray(utf8))
- }
- }
- }
-
- messageDigest.update(kind.motivationURI.toByteArray(utf8))
-
- val digestResult = messageDigest.digest()
- val builder = StringBuilder(64)
- for (index in digestResult.indices) {
- val bb = digestResult[index]
- builder.append(String.format("%02x", bb))
- }
-
- return BookmarkID(builder.toString())
- } catch (e: NoSuchAlgorithmException) {
- throw IllegalStateException(e)
- }
- }
-
- companion object {
-
- fun create(
- opdsId: String,
- location: BookLocation,
- kind: BookmarkKind,
- time: DateTime,
- chapterTitle: String,
- bookProgress: Double?,
- deviceID: String,
- uri: URI?
- ): ReaderBookmark {
- return ReaderBookmark(
- opdsId = opdsId,
- location = location,
- kind = kind,
- time = time.toDateTime(DateTimeZone.UTC),
- chapterTitle = chapterTitle,
- bookProgress = bookProgress,
- deviceID = deviceID,
- uri = uri
- )
- }
- }
- }
-
- /**
- * Class for bookmarks of PDF type.
- *
- * Note: The type is {@link Serializable} purely because the Android API requires this
- * in order pass values of this type between activities. We make absolutely no guarantees
- * that serialized values of this class will be compatible with future releases.
- */
-
- data class PDFBookmark(
- override val opdsId: String,
- override val time: DateTime,
- override val deviceID: String,
- override val kind: BookmarkKind,
- override val uri: URI?,
- val pageNumber: Int
- ) : Bookmark(), Serializable {
-
- override val book: BookID = BookIDs.newFromText(this.opdsId)
-
- override val bookmarkId: BookmarkID = createBookmarkID(this.book, this.kind, this.pageNumber)
-
- override fun toLastReadLocation(): Bookmark {
- return this.copy(kind = BookmarkKind.BookmarkLastReadLocation)
- }
-
- override fun toExplicit(): Bookmark {
- return this.copy(kind = BookmarkKind.BookmarkExplicit)
- }
-
- init {
- check(this.time.zone == DateTimeZone.UTC) {
- "Bookmark time zones must be UTC"
- }
- }
-
- /**
- * Create a bookmark ID from the given book ID, kind and page number.
- */
-
- private fun createBookmarkID(
- book: BookID,
- kind: BookmarkKind,
- pageNumber: Int
- ): BookmarkID {
- try {
- val messageDigest = MessageDigest.getInstance("SHA-256")
- val utf8 = Charset.forName("UTF-8")
- messageDigest.update(book.value().toByteArray(utf8))
- messageDigest.update(kind.motivationURI.toByteArray(utf8))
- messageDigest.update(pageNumber.toString().toByteArray(utf8))
-
- val digestResult = messageDigest.digest()
- val builder = StringBuilder(64)
- for (index in digestResult.indices) {
- val bb = digestResult[index]
- builder.append(String.format("%02x", bb))
- }
-
- return BookmarkID(builder.toString())
- } catch (e: NoSuchAlgorithmException) {
- throw IllegalStateException(e)
- }
- }
-
- companion object {
-
- fun create(
- opdsId: String,
- kind: BookmarkKind,
- time: DateTime,
- pageNumber: Int,
- deviceID: String,
- uri: URI?
- ): PDFBookmark {
- return PDFBookmark(
- opdsId = opdsId,
- pageNumber = pageNumber,
- kind = kind,
- time = time.toDateTime(DateTimeZone.UTC),
- deviceID = deviceID,
- uri = uri
- )
- }
- }
- }
-
- /**
- * Class for bookmarks of audiobook type.
- *
- * Note: The type is {@link Serializable} purely because the Android API requires this
- * in order pass values of this type between activities. We make absolutely no guarantees
- * that serialized values of this class will be compatible with future releases.
- */
-
- data class AudiobookBookmark(
- override val opdsId: String,
- override val time: DateTime,
- override val deviceID: String,
- override val kind: BookmarkKind,
- override val uri: URI?,
-
- /**
- * The location of the bookmark.
- */
-
- val location: PlayerPosition,
-
- /**
- * The duration of the bookmark's chapter.
- */
- val duration: Long
-
- ) : Bookmark() {
-
- override val book: BookID = BookIDs.newFromText(this.opdsId)
-
- override val bookmarkId: BookmarkID = createBookmarkID(this.book, this.kind, this.location)
-
- override fun toLastReadLocation(): Bookmark {
- return this.copy(kind = BookmarkKind.BookmarkLastReadLocation)
- }
-
- override fun toExplicit(): Bookmark {
- return this.copy(kind = BookmarkKind.BookmarkExplicit)
- }
-
- init {
- check(this.time.zone == DateTimeZone.UTC) {
- "Bookmark time zones must be UTC"
- }
- }
-
- /**
- * Create a bookmark ID from the given book ID, kind and location.
- */
-
- private fun createBookmarkID(
- book: BookID,
- kind: BookmarkKind,
- location: PlayerPosition
- ): BookmarkID {
- try {
- val messageDigest = MessageDigest.getInstance("SHA-256")
- val utf8 = Charset.forName("UTF-8")
- messageDigest.update(book.value().toByteArray(utf8))
- messageDigest.update(kind.motivationURI.toByteArray(utf8))
- messageDigest.update(location.chapter.toString().toByteArray(utf8))
- messageDigest.update(location.part.toString().toByteArray(utf8))
- messageDigest.update(location.startOffset.toString().toByteArray(utf8))
- messageDigest.update(location.currentOffset.toString().toByteArray(utf8))
-
- val digestResult = messageDigest.digest()
- val builder = StringBuilder(64)
- for (index in digestResult.indices) {
- val bb = digestResult[index]
- builder.append(String.format("%02x", bb))
- }
-
- return BookmarkID(builder.toString())
- } catch (e: NoSuchAlgorithmException) {
- throw IllegalStateException(e)
- }
- }
-
- companion object {
-
- fun create(
- opdsId: String,
- location: PlayerPosition,
- kind: BookmarkKind,
- time: DateTime,
- deviceID: String,
- duration: Long,
- uri: URI?
- ): AudiobookBookmark {
- return AudiobookBookmark(
- opdsId = opdsId,
- location = location,
- kind = kind,
- time = time.toDateTime(DateTimeZone.UTC),
- deviceID = deviceID,
- duration = duration,
- uri = uri
- )
- }
- }
- }
-}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkDigests.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkDigests.kt
new file mode 100644
index 000000000..cdbb03517
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkDigests.kt
@@ -0,0 +1,27 @@
+package org.nypl.simplified.books.api.bookmark
+
+import java.nio.charset.StandardCharsets.UTF_8
+import java.security.MessageDigest
+
+object BookmarkDigests {
+ fun addToDigest(
+ digest: MessageDigest,
+ text: String
+ ) {
+ digest.update(text.toByteArray(UTF_8))
+ }
+
+ fun addToDigest(
+ digest: MessageDigest,
+ number: Int
+ ) {
+ digest.update(number.toString().toByteArray(UTF_8))
+ }
+
+ fun addToDigest(
+ digest: MessageDigest,
+ number: Double
+ ) {
+ addToDigest(digest, String.format("%.6f", number))
+ }
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkID.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkID.kt
index 1910c086f..d96205083 100644
--- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkID.kt
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkID.kt
@@ -1,6 +1,5 @@
package org.nypl.simplified.books.api.bookmark
-import java.io.Serializable
import java.util.regex.Pattern
/**
@@ -11,7 +10,9 @@ import java.util.regex.Pattern
* that serialized values of this class will be compatible with future releases.
*/
-data class BookmarkID(val value: String) : Serializable {
+data class BookmarkID(
+ val value: String
+) {
init {
if (!VALID_BOOKMARK_ID.matcher(value).matches()) {
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkJSON.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkJSON.kt
deleted file mode 100644
index 8eb0080d2..000000000
--- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkJSON.kt
+++ /dev/null
@@ -1,669 +0,0 @@
-package org.nypl.simplified.books.api.bookmark
-
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.databind.node.ArrayNode
-import com.fasterxml.jackson.databind.node.ObjectNode
-import com.io7m.jfunctional.OptionType
-import com.io7m.jfunctional.Some
-import org.joda.time.DateTime
-import org.joda.time.DateTimeZone
-import org.joda.time.format.ISODateTimeFormat
-import org.librarysimplified.audiobook.api.PlayerPosition
-import org.librarysimplified.audiobook.api.PlayerPositions
-import org.librarysimplified.audiobook.api.PlayerResult
-import org.nypl.simplified.books.api.BookLocation
-import org.nypl.simplified.books.api.helper.ReaderLocationJSON
-import org.nypl.simplified.json.core.JSONParseException
-import org.nypl.simplified.json.core.JSONParserUtilities
-import org.nypl.simplified.json.core.JSONSerializerUtilities
-import org.nypl.simplified.opds.core.getOrNull
-import java.io.ByteArrayOutputStream
-import java.io.IOException
-
-/**
- * Functions to serialize bookmarks to/from JSON.
- */
-
-object BookmarkJSON {
-
- private val dateFormatter =
- ISODateTimeFormat.dateTime()
- .withZoneUTC()
-
- private val dateParserWithTimezone =
- ISODateTimeFormat.dateTimeParser()
- .withOffsetParsed()
-
- private val dateParserWithUTC =
- ISODateTimeFormat.dateTimeParser()
- .withZoneUTC()
-
- /**
- * Deserialize bookmarks from the given JSON node.
- *
- * @param kind The kind of bookmark
- * @param node A JSON node
- * @return A reader bookmark
- * @throws JSONParseException On parse errors
- */
-
- @JvmStatic
- @Throws(JSONParseException::class)
- fun deserializeReaderBookmarkFromJSON(
- kind: BookmarkKind,
- node: JsonNode
- ): Bookmark.ReaderBookmark {
- return deserializeReaderBookmarkFromJSON(
- kind = kind,
- node = JSONParserUtilities.checkObject(null, node)
- )
- }
-
- /**
- * Deserialize bookmarks from the given JSON node.
- *
- * @param kind The kind of bookmark
- * @param node A JSON node
- * @return A pdf bookmark
- * @throws JSONParseException On parse errors
- */
-
- @JvmStatic
- @Throws(JSONParseException::class)
- fun deserializePdfBookmarkFromJSON(
- kind: BookmarkKind,
- node: JsonNode
- ): Bookmark.PDFBookmark {
- return deserializePdfBookmarkFromJSON(
- kind = kind,
- node = JSONParserUtilities.checkObject(null, node)
- )
- }
-
- /**
- * Deserialize bookmarks from the given JSON node.
- *
- * @param kind The kind of bookmark
- * @param node A JSON node
- * @return A reader bookmark
- * @throws JSONParseException On parse errors
- */
-
- @JvmStatic
- @Throws(JSONParseException::class)
- fun deserializeReaderBookmarkFromJSON(
- kind: BookmarkKind,
- node: ObjectNode
- ): Bookmark.ReaderBookmark {
- return when (val version = JSONParserUtilities.getIntegerOrNull(node, "@version")) {
- 20210828 ->
- deserializeReaderBookmarkFromJSON20210828(kind, node)
- 20210317 ->
- deserializeReaderBookmarkFromJSON20210828(kind, node)
- null ->
- deserializeReaderBookmarkFromJSONOld(kind, node)
- else ->
- throw JSONParseException("Unsupported bookmark version: $version")
- }
- }
-
- /**
- * Deserialize bookmarks from the given JSON node.
- *
- * @param kind The kind of bookmark
- * @param node A JSON node
- * @return A pdf bookmark
- * @throws JSONParseException On parse errors
- */
-
- @JvmStatic
- @Throws(JSONParseException::class)
- fun deserializePdfBookmarkFromJSON(
- kind: BookmarkKind,
- node: ObjectNode
- ): Bookmark.PDFBookmark {
- return when (val version = JSONParserUtilities.getIntegerOrNull(node, "@version")) {
- 1 ->
- deserializePdfBookmarkFromJSONVersion1(kind, node)
- else ->
- throw JSONParseException("Unsupported bookmark version: $version")
- }
- }
-
- /**
- * Deserialize bookmarks from the given JSON node.
- *
- * @param objectMapper A JSON object mapper
- * @param kind The kind of bookmark
- * @param node A JSON node
- * @return An audiobook bookmark
- * @throws JSONParseException On parse errors
- */
-
- @JvmStatic
- @Throws(JSONParseException::class)
- fun deserializeFromJSON(
- objectMapper: ObjectMapper,
- kind: BookmarkKind,
- node: JsonNode
- ): Bookmark.AudiobookBookmark {
- return deserializeFromJSON(
- objectMapper = objectMapper,
- kind = kind,
- node = JSONParserUtilities.checkObject(null, node)
- )
- }
-
- /**
- * Deserialize bookmarks from the given JSON node.
- *
- * @param kind The kind of bookmark
- * @param node A JSON node
- * @return An audiobook bookmark
- * @throws JSONParseException On parse errors
- */
-
- @JvmStatic
- @Throws(JSONParseException::class)
- fun deserializeAudiobookBookmarkFromJSON(
- kind: BookmarkKind,
- node: ObjectNode
- ): Bookmark.AudiobookBookmark {
- val locationResult = PlayerPositions.parseFromObjectNode(node)
- val location: PlayerPosition
-
- when (locationResult) {
- is PlayerResult.Success -> {
- location = locationResult.result
- }
- is PlayerResult.Failure -> throw locationResult.failure
- }
-
- val parsedTime = parseTime(
- JSONParserUtilities.getStringDefault(node, "time", dateFormatter.print(DateTime.now()))
- )
-
- return Bookmark.AudiobookBookmark.create(
- opdsId = JSONParserUtilities.getStringDefault(node, "opdsId", ""),
- kind = kind,
- location = location,
- duration = 0L,
- time = parsedTime,
- uri = toNullable(JSONParserUtilities.getURIOptional(node, "uri")),
- deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", "")
- )
- }
-
- private fun deserializePdfBookmarkFromJSONVersion1(
- kind: BookmarkKind,
- node: ObjectNode
- ): Bookmark.PDFBookmark {
- val locationObj = JSONParserUtilities.getObject(node, "location")
- val pageIndex =
- JSONParserUtilities.getIntegerOrNull(locationObj, "page") ?: 1
-
- val timeParsed =
- parseTime(JSONParserUtilities.getString(node, "time"))
-
- return Bookmark.PDFBookmark.create(
- opdsId = JSONParserUtilities.getString(node, "opdsId"),
- kind = kind,
- pageNumber = pageIndex,
- time = timeParsed,
- uri = toNullable(JSONParserUtilities.getURIOptional(node, "uri")),
- deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", null)
- )
- }
-
- private fun deserializeReaderBookmarkFromJSON20210828(
- kind: BookmarkKind,
- node: ObjectNode
- ): Bookmark.ReaderBookmark {
- val location =
- ReaderLocationJSON.deserializeFromJSON(
- JSONParserUtilities.getObject(node, "location")
- )
-
- val timeParsed =
- parseTime(JSONParserUtilities.getString(node, "time"))
-
- return Bookmark.ReaderBookmark.create(
- opdsId = JSONParserUtilities.getString(node, "opdsId"),
- kind = kind,
- location = location,
- time = timeParsed,
- chapterTitle = JSONParserUtilities.getString(node, "chapterTitle"),
- bookProgress = JSONParserUtilities.getDoubleDefault(node, "bookProgress", 0.0),
- uri = toNullable(JSONParserUtilities.getURIOptional(node, "uri")),
- deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", null)
- )
- }
-
- private fun deserializeReaderBookmarkFromJSONOld(
- kind: BookmarkKind,
- node: ObjectNode
- ): Bookmark.ReaderBookmark {
- val location =
- ReaderLocationJSON.deserializeFromJSON(
- JSONParserUtilities.getObject(node, "location")
- )
-
- /*
- * Old bookmarks have a top-level chapterProgress value. We've moved to having this
- * stored explicitly in book locations for modern bookmarks. We pick whichever is
- * the greater of the two possible values, because we default to 0.0 for missing
- * values.
- */
-
- val chapterProgress =
- JSONParserUtilities.getDoubleDefault(node, "chapterProgress", 0.0)
-
- val locationMax =
- when (location) {
- is BookLocation.BookLocationR2 ->
- location
- is BookLocation.BookLocationR1 ->
- location.copy(progress = Math.max(location.progress ?: 0.0, chapterProgress))
- }
-
- return Bookmark.ReaderBookmark(
- opdsId = JSONParserUtilities.getString(node, "opdsId"),
- kind = kind,
- location = locationMax,
- time = parseTime(JSONParserUtilities.getString(node, "time")),
- chapterTitle = JSONParserUtilities.getString(node, "chapterTitle"),
- bookProgress = JSONParserUtilities.getDoubleOptional(node, "bookProgress").getOrNull(),
- uri = toNullable(JSONParserUtilities.getURIOptional(node, "uri")),
- deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", null)
- )
- }
-
- /**
- * Correctly parse a date/time value.
- *
- * This slightly odd function first attempts to parse the incoming string as if it was
- * a date/time string with an included time zone. If the time string turned out not to
- * include a time zone, Joda Time will parse it using the system's default timezone. We
- * then detect that this has happened and, if the current system's timezone isn't UTC,
- * we parse the string *again* but this time assuming a UTC timezone.
- */
-
- private fun parseTime(
- timeText: String
- ): DateTime {
- val defaultZone = DateTimeZone.getDefault()
- val timeParsedWithZone = dateParserWithTimezone.parseDateTime(timeText)
- if (timeParsedWithZone.zone == defaultZone && defaultZone != DateTimeZone.UTC) {
- return dateParserWithUTC.parseDateTime(timeText)
- }
- return timeParsedWithZone.toDateTime(DateTimeZone.UTC)
- }
-
- private fun toNullable(option: OptionType): T? {
- return if (option is Some) {
- option.get()
- } else {
- null
- }
- }
-
- /**
- * Serialize a bookmark to JSON.
- *
- * @param objectMapper A JSON object mapper
- * @param bookmark A reader bookmark
- * @return A serialized object
- */
-
- @JvmStatic
- fun serializeReaderBookmarkToJSON(
- objectMapper: ObjectMapper,
- bookmark: Bookmark.ReaderBookmark
- ): ObjectNode {
- val node = objectMapper.createObjectNode()
- node.put("@version", 20210828)
- node.put("opdsId", bookmark.opdsId)
- val location = ReaderLocationJSON.serializeToJSON(objectMapper, bookmark.location)
- node.set("location", location)
- node.put("time", dateFormatter.print(bookmark.time))
- node.put("chapterTitle", bookmark.chapterTitle)
- bookmark.bookProgress?.let { node.put("bookProgress", it) }
- bookmark.deviceID.let { device -> node.put("deviceID", device) }
- return node
- }
-
- /**
- * Serialize a bookmark to JSON.
- *
- * @param objectMapper A JSON object mapper
- * @param bookmark A pdf bookmark
- * @return A serialized object
- */
-
- @JvmStatic
- fun serializePdfBookmarkToJSON(
- objectMapper: ObjectMapper,
- bookmark: Bookmark.PDFBookmark
- ): ObjectNode {
- val node = objectMapper.createObjectNode()
- node.put("@version", 1)
- node.put("opdsId", bookmark.opdsId)
-
- val locationObject = objectMapper.createObjectNode()
- locationObject.put("page", bookmark.pageNumber)
- node.set("location", locationObject)
- node.put("time", dateFormatter.print(bookmark.time))
- bookmark.deviceID.let { device -> node.put("deviceID", device) }
- return node
- }
-
- /**
- * Serialize a bookmark to JSON.
- *
- * @param objectMapper A JSON object mapper
- * @param bookmarks A list of reader bookmarks
- * @return A serialized object
- */
-
- @JvmStatic
- fun serializeReaderBookmarksToJSON(
- objectMapper: ObjectMapper,
- bookmarks: List
- ): ArrayNode {
- val node = objectMapper.createArrayNode()
- bookmarks.forEach { bookmark ->
- node.add(
- serializeReaderBookmarkToJSON(
- objectMapper,
- bookmark
- )
- )
- }
- return node
- }
-
- /**
- * Serialize a bookmark to JSON.
- *
- * @param objectMapper A JSON object mapper
- * @param bookmarks A list of pdf bookmarks
- * @return A serialized object
- */
-
- @JvmStatic
- fun serializePdfBookmarksToJSON(
- objectMapper: ObjectMapper,
- bookmarks: List
- ): ArrayNode {
- val node = objectMapper.createArrayNode()
- bookmarks.forEach { bookmark ->
- node.add(
- serializePdfBookmarkToJSON(
- objectMapper,
- bookmark
- )
- )
- }
- return node
- }
-
- /**
- * Serialize a bookmark to a JSON string.
- *
- * @param objectMapper A JSON object mapper
- * @param bookmark A reader bookmark
- * @return A JSON string
- * @throws IOException On serialization errors
- */
-
- @JvmStatic
- @Throws(IOException::class)
- fun serializeReaderBookmarkToString(
- objectMapper: ObjectMapper,
- bookmark: Bookmark.ReaderBookmark
- ): String {
- val json = serializeReaderBookmarkToJSON(objectMapper, bookmark)
- val output = ByteArrayOutputStream(1024)
- JSONSerializerUtilities.serialize(json, output)
- return output.toString("UTF-8")
- }
-
- /**
- * Serialize a bookmark to a JSON string.
- *
- * @param objectMapper A JSON object mapper
- * @param bookmark A pdf bookmark
- * @return A JSON string
- * @throws IOException On serialization errors
- */
-
- @JvmStatic
- @Throws(IOException::class)
- fun serializePdfBookmarkToString(
- objectMapper: ObjectMapper,
- bookmark: Bookmark.PDFBookmark
- ): String {
- val json = serializePdfBookmarkToJSON(objectMapper, bookmark)
- val output = ByteArrayOutputStream(1024)
- JSONSerializerUtilities.serialize(json, output)
- return output.toString("UTF-8")
- }
-
- /**
- * Serialize a bookmark to a JSON string.
- *
- * @param objectMapper A JSON object mapper
- * @param bookmarks A list of reader bookmarks
- * @return A JSON string
- * @throws IOException On serialization errors
- */
-
- @JvmStatic
- @Throws(IOException::class)
- fun serializeReaderBookmarksToString(
- objectMapper: ObjectMapper,
- bookmarks: List
- ): String {
- val json = serializeReaderBookmarksToJSON(objectMapper, bookmarks)
- val output = ByteArrayOutputStream(1024)
- val writer = objectMapper.writerWithDefaultPrettyPrinter()
- writer.writeValue(output, json)
- return output.toString("UTF-8")
- }
-
- /**
- * Serialize a bookmark to a JSON string.
- *
- * @param objectMapper A JSON object mapper
- * @param bookmarks A list of pdf bookmarks
- * @return A JSON string
- * @throws IOException On serialization errors
- */
-
- @JvmStatic
- @Throws(IOException::class)
- fun serializePdfBookmarksToString(
- objectMapper: ObjectMapper,
- bookmarks: List
- ): String {
- val json = serializePdfBookmarksToJSON(objectMapper, bookmarks)
- val output = ByteArrayOutputStream(1024)
- val writer = objectMapper.writerWithDefaultPrettyPrinter()
- writer.writeValue(output, json)
- return output.toString("UTF-8")
- }
-
- /**
- * Deserialize a bookmark from the given string.
- *
- * @param objectMapper A JSON object mapper
- * @param kind The kind of bookmark
- * @param serialized A serialized JSON string
- * @return A reader bookmark
- * @throws IOException On I/O or parser errors
- */
-
- @JvmStatic
- @Throws(IOException::class)
- fun deserializeReaderBookmarkFromString(
- objectMapper: ObjectMapper,
- kind: BookmarkKind,
- serialized: String
- ): Bookmark.ReaderBookmark {
- return deserializeReaderBookmarkFromJSON(
- kind = kind,
- node = objectMapper.readTree(serialized)
- )
- }
-
- /**
- * Deserialize a bookmark from the given string.
- *
- * @param objectMapper A JSON object mapper
- * @param kind The kind of bookmark
- * @param serialized A serialized JSON string
- * @return A pdf bookmark
- * @throws IOException On I/O or parser errors
- */
-
- @JvmStatic
- @Throws(IOException::class)
- fun deserializePdfBookmarkFromString(
- objectMapper: ObjectMapper,
- kind: BookmarkKind,
- serialized: String
- ): Bookmark.PDFBookmark {
- return deserializePdfBookmarkFromJSON(
- kind = kind,
- node = objectMapper.readTree(serialized)
- )
- }
-
- /**
- * Serialize a bookmark to JSON.
- *
- * @param bookmark An audiobook bookmark
- * @return A serialized object
- */
-
- @JvmStatic
- fun serializeAudiobookBookmarkToJSON(
- bookmark: Bookmark.AudiobookBookmark
- ): ObjectNode {
- val node = PlayerPositions.serializeToObjectNode(bookmark.location)
- node.put("opdsId", bookmark.opdsId)
- node.put("time", dateFormatter.print(bookmark.time))
- bookmark.deviceID.let { device -> node.put("deviceID", device) }
- return node
- }
-
- /**
- * Serialize a bookmark to JSON.
- *
- * @param objectMapper A JSON object mapper
- * @param bookmarks A list of audiobook bookmarks object mapper
- * @return A serialized object
- */
-
- @JvmStatic
- fun serializeAudiobookBookmarksToJSON(
- objectMapper: ObjectMapper,
- bookmarks: List
- ): ArrayNode {
- val node = objectMapper.createArrayNode()
- bookmarks.forEach { bookmark ->
- node.add(
- serializeAudiobookBookmarkToJSON(
- bookmark
- )
- )
- }
- return node
- }
-
- /**
- * Serialize a bookmark to a JSON string.
- *
- * @param bookmark An audiobook bookmark
- * @return A JSON string
- * @throws IOException On serialization errors
- */
-
- @JvmStatic
- @Throws(IOException::class)
- fun serializeAudiobookBookmarkToString(
- bookmark: Bookmark.AudiobookBookmark
- ): String {
- val json = serializeAudiobookBookmarkToJSON(bookmark)
- val output = ByteArrayOutputStream(1024)
- JSONSerializerUtilities.serialize(json, output)
- return output.toString("UTF-8")
- }
-
- /**
- * Serialize a bookmark to a JSON string.
- *
- * @param objectMapper A JSON object mapper
- * @param bookmarks A list of audiobook bookmarks
- * @return A JSON string
- * @throws IOException On serialization errors
- */
-
- @JvmStatic
- @Throws(IOException::class)
- fun serializeAudiobookBookmarksToString(
- objectMapper: ObjectMapper,
- bookmarks: List
- ): String {
- val json = serializeAudiobookBookmarksToJSON(objectMapper, bookmarks)
- val output = ByteArrayOutputStream(1024)
- val writer = objectMapper.writerWithDefaultPrettyPrinter()
- writer.writeValue(output, json)
- return output.toString("UTF-8")
- }
-
- /**
- * Deserialize a bookmark from the given string.
- *
- * @param objectMapper A JSON object mapper
- * @param kind The kind of bookmark
- * @param serialized A serialized JSON string
- * @return A parsed location
- * @throws IOException On I/O or parser errors
- */
-
- @JvmStatic
- @Throws(IOException::class)
- fun deserializeAudiobookBookmarkFromString(
- objectMapper: ObjectMapper,
- kind: BookmarkKind,
- serialized: String
- ): Bookmark.AudiobookBookmark {
- return deserializeAudiobookBookmarkFromJSON(
- kind = kind,
- node = objectMapper.readTree(serialized)
- )
- }
-
- /**
- * Deserialize bookmarks from the given JSON node.
- *
- * @param kind The kind of bookmark
- * @param node A JSON node
- * @return A parsed description
- * @throws JSONParseException On parse errors
- */
-
- @JvmStatic
- @Throws(JSONParseException::class)
- fun deserializeAudiobookBookmarkFromJSON(
- kind: BookmarkKind,
- node: JsonNode
- ): Bookmark.AudiobookBookmark {
- return deserializeAudiobookBookmarkFromJSON(
- kind = kind,
- node = JSONParserUtilities.checkObject(null, node)
- )
- }
-}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkMetadata.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkMetadata.kt
new file mode 100644
index 000000000..81575c041
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkMetadata.kt
@@ -0,0 +1,43 @@
+package org.nypl.simplified.books.api.bookmark
+
+import org.joda.time.DateTime
+import org.joda.time.format.ISODateTimeFormat
+import java.net.URI
+import java.security.MessageDigest
+
+/**
+ * The non-critical metadata for a bookmark.
+ */
+
+data class BookmarkMetadata(
+ val bookChapterProgress: Double,
+ val bookChapterTitle: String,
+ val bookOpdsId: String,
+ val bookProgress: Double,
+ val bookTitle: String,
+ val deviceID: String,
+ val time: DateTime,
+ val uri: URI?,
+) {
+
+ private val dateFormatter =
+ ISODateTimeFormat.dateTime()
+ .withZoneUTC()
+
+ /**
+ * Add the fields of this object to the given message digest
+ */
+
+ fun addToDigest(
+ digest: MessageDigest
+ ) {
+ BookmarkDigests.addToDigest(digest, this.bookChapterProgress)
+ BookmarkDigests.addToDigest(digest, this.bookChapterTitle)
+ BookmarkDigests.addToDigest(digest, this.bookOpdsId)
+ BookmarkDigests.addToDigest(digest, this.bookProgress)
+ BookmarkDigests.addToDigest(digest, this.bookTitle)
+ BookmarkDigests.addToDigest(digest, this.deviceID)
+ BookmarkDigests.addToDigest(digest, this.dateFormatter.print(this.time))
+ this.uri.let { x -> BookmarkDigests.addToDigest(digest, x.toString()) }
+ }
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark.kt
new file mode 100644
index 000000000..b60aeee44
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark.kt
@@ -0,0 +1,125 @@
+package org.nypl.simplified.books.api.bookmark
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import org.joda.time.DateTime
+import org.nypl.simplified.books.api.BookID
+import java.net.URI
+import java.security.MessageDigest
+
+/**
+ * The type of serialized bookmarks.
+ */
+
+sealed class SerializedBookmark {
+
+ /**
+ * The type name (such as "Bookmark")
+ */
+
+ abstract val typeName: String
+
+ /**
+ * The type version (such as 20210317)
+ */
+
+ abstract val typeVersion: Int
+
+ /**
+ * @return The identifier of the book taken from the OPDS entry that provided it.
+ */
+
+ abstract val opdsId: String
+
+ /**
+ * @return The kind of bookmark.
+ */
+
+ abstract val kind: BookmarkKind
+
+ /**
+ * @return The time the bookmark was created.
+ */
+
+ abstract val time: DateTime
+
+ /**
+ * @return The identifier of the device that created the bookmark, if one is available.
+ */
+
+ abstract val deviceID: String
+
+ /**
+ * @return The URI of this bookmark, if the bookmark exists on a remote server.
+ */
+
+ abstract val uri: URI?
+
+ /**
+ * The ID of the book to which the bookmark belongs.
+ */
+
+ abstract val book: BookID
+
+ /**
+ * The unique ID of the bookmark.
+ */
+
+ abstract val bookmarkId: BookmarkID
+
+ /**
+ * The bookmark location
+ */
+
+ abstract val location: SerializedLocator
+
+ /**
+ * The progress of the bookmark throughout the entire book
+ */
+
+ abstract val bookProgress: Double
+
+ /**
+ * The progress of the bookmark throughout the chapter
+ */
+
+ abstract val bookChapterProgress: Double
+
+ /**
+ * The book title
+ */
+
+ abstract val bookTitle: String
+
+ /**
+ * The book chapter title
+ */
+
+ abstract val bookChapterTitle: String
+
+ /**
+ * Serialize this bookmark to JSON.
+ */
+
+ abstract fun toJSON(
+ objectMapper: ObjectMapper
+ ): ObjectNode
+
+ /**
+ * Add the fields of this object to the given message digest
+ */
+
+ abstract fun addToDigest(digest: MessageDigest)
+
+ /**
+ * @return This bookmark with the given URI
+ */
+
+ abstract fun withURI(uri: URI): SerializedBookmark
+
+ /**
+ * @return This bookmark without the URI
+ */
+
+ abstract fun withoutURI(): SerializedBookmark
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210317.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210317.kt
new file mode 100644
index 000000000..2ca53b9ac
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210317.kt
@@ -0,0 +1,87 @@
+package org.nypl.simplified.books.api.bookmark
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import org.joda.time.DateTime
+import org.joda.time.format.ISODateTimeFormat
+import org.nypl.simplified.books.api.BookID
+import org.nypl.simplified.books.api.BookIDs
+import java.net.URI
+import java.nio.charset.StandardCharsets.UTF_8
+import java.security.MessageDigest
+
+data class SerializedBookmark20210317(
+ override val bookChapterProgress: Double,
+ override val bookChapterTitle: String,
+ override val bookProgress: Double,
+ override val bookTitle: String,
+ override val deviceID: String,
+ override val kind: BookmarkKind,
+ override val location: SerializedLocator,
+ override val opdsId: String,
+ override val time: DateTime,
+ override val uri: URI?,
+) : SerializedBookmark() {
+
+ private val dateFormatter =
+ ISODateTimeFormat.dateTime()
+ .withZoneUTC()
+
+ override val typeName: String
+ get() = "Bookmark"
+
+ override val typeVersion: Int
+ get() = 20210317
+
+ override val book: BookID
+ get() = BookIDs.newFromText(this.opdsId)
+
+ override val bookmarkId: BookmarkID
+ get() = this.createBookmarkId()
+
+ private fun createBookmarkId(): BookmarkID {
+ val digest = MessageDigest.getInstance("SHA-256")
+ this.addToDigest(digest)
+
+ val digestResult = digest.digest()
+ val builder = StringBuilder(64)
+ for (index in digestResult.indices) {
+ val bb = digestResult[index]
+ builder.append(String.format("%02x", bb))
+ }
+ return BookmarkID(builder.toString())
+ }
+
+ override fun toJSON(
+ objectMapper: ObjectMapper
+ ): ObjectNode {
+ val node = objectMapper.createObjectNode()
+ node.put("@type", this.typeName)
+ node.put("@version", this.typeVersion)
+
+ val location = this.location.toJSON(objectMapper)
+ node.set("location", location)
+ node.put("time", this.dateFormatter.print(this.time))
+ node.put("kind", this.kind.toString())
+ node.put("opdsId", this.opdsId)
+ this.deviceID.let { device -> node.put("deviceID", device) }
+ this.uri.let { u -> node.put("uri", u.toString()) }
+ return node
+ }
+
+ override fun addToDigest(
+ digest: MessageDigest
+ ) {
+ BookmarkDigests.addToDigest(digest, this.opdsId)
+ this.location.addToDigest(digest)
+ digest.update(this.dateFormatter.print(this.time).toByteArray(UTF_8))
+ }
+
+ override fun withURI(uri: URI): SerializedBookmark {
+ return this.copy(uri = uri)
+ }
+
+ override fun withoutURI(): SerializedBookmark {
+ return this.copy(uri = null)
+ }
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210828.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210828.kt
new file mode 100644
index 000000000..50e953ac8
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210828.kt
@@ -0,0 +1,87 @@
+package org.nypl.simplified.books.api.bookmark
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import org.joda.time.DateTime
+import org.joda.time.format.ISODateTimeFormat
+import org.nypl.simplified.books.api.BookID
+import org.nypl.simplified.books.api.BookIDs
+import java.net.URI
+import java.nio.charset.StandardCharsets.UTF_8
+import java.security.MessageDigest
+
+data class SerializedBookmark20210828(
+ override val bookChapterProgress: Double,
+ override val bookChapterTitle: String,
+ override val bookProgress: Double,
+ override val bookTitle: String,
+ override val deviceID: String,
+ override val kind: BookmarkKind,
+ override val location: SerializedLocator,
+ override val opdsId: String,
+ override val time: DateTime,
+ override val uri: URI?,
+) : SerializedBookmark() {
+
+ private val dateFormatter =
+ ISODateTimeFormat.dateTime()
+ .withZoneUTC()
+
+ override val typeName: String
+ get() = "Bookmark"
+
+ override val typeVersion: Int
+ get() = 20210828
+
+ override val book: BookID
+ get() = BookIDs.newFromText(this.opdsId)
+
+ override val bookmarkId: BookmarkID
+ get() = this.createBookmarkId()
+
+ private fun createBookmarkId(): BookmarkID {
+ val digest = MessageDigest.getInstance("SHA-256")
+ this.addToDigest(digest)
+
+ val digestResult = digest.digest()
+ val builder = StringBuilder(64)
+ for (index in digestResult.indices) {
+ val bb = digestResult[index]
+ builder.append(String.format("%02x", bb))
+ }
+ return BookmarkID(builder.toString())
+ }
+
+ override fun toJSON(
+ objectMapper: ObjectMapper
+ ): ObjectNode {
+ val node = objectMapper.createObjectNode()
+ node.put("@type", this.typeName)
+ node.put("@version", this.typeVersion)
+
+ val location = this.location.toJSON(objectMapper)
+ node.set("location", location)
+ node.put("time", dateFormatter.print(this.time))
+ node.put("kind", this.kind.toString())
+ node.put("opdsId", this.opdsId)
+ this.deviceID.let { device -> node.put("deviceID", device) }
+ this.uri.let { u -> node.put("uri", u.toString()) }
+ return node
+ }
+
+ override fun addToDigest(
+ digest: MessageDigest
+ ) {
+ BookmarkDigests.addToDigest(digest, this.opdsId)
+ this.location.addToDigest(digest)
+ digest.update(this.dateFormatter.print(this.time).toByteArray(UTF_8))
+ }
+
+ override fun withURI(uri: URI): SerializedBookmark {
+ return this.copy(uri = uri)
+ }
+
+ override fun withoutURI(): SerializedBookmark {
+ return this.copy(uri = null)
+ }
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20240424.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20240424.kt
new file mode 100644
index 000000000..1a5707854
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20240424.kt
@@ -0,0 +1,95 @@
+package org.nypl.simplified.books.api.bookmark
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import org.joda.time.DateTime
+import org.joda.time.format.ISODateTimeFormat
+import org.nypl.simplified.books.api.BookID
+import org.nypl.simplified.books.api.BookIDs
+import java.net.URI
+import java.nio.charset.StandardCharsets.UTF_8
+import java.security.MessageDigest
+
+data class SerializedBookmark20240424(
+ override val bookChapterProgress: Double,
+ override val bookChapterTitle: String,
+ override val bookProgress: Double,
+ override val bookTitle: String,
+ override val deviceID: String,
+ override val kind: BookmarkKind,
+ override val location: SerializedLocator,
+ override val opdsId: String,
+ override val time: DateTime,
+ override val uri: URI?,
+) : SerializedBookmark() {
+
+ private val dateFormatter =
+ ISODateTimeFormat.dateTime()
+ .withZoneUTC()
+
+ override val typeName: String
+ get() = "Bookmark"
+
+ override val typeVersion: Int
+ get() = 20240424
+
+ override val book: BookID
+ get() = BookIDs.newFromText(this.opdsId)
+
+ override val bookmarkId: BookmarkID
+ get() = this.createBookmarkId()
+
+ private fun createBookmarkId(): BookmarkID {
+ val digest = MessageDigest.getInstance("SHA-256")
+ this.addToDigest(digest)
+
+ val digestResult = digest.digest()
+ val builder = StringBuilder(64)
+ for (index in digestResult.indices) {
+ val bb = digestResult[index]
+ builder.append(String.format("%02x", bb))
+ }
+ return BookmarkID(builder.toString())
+ }
+
+ override fun toJSON(
+ objectMapper: ObjectMapper
+ ): ObjectNode {
+ val node = objectMapper.createObjectNode()
+ node.put("@type", this.typeName)
+ node.put("@version", this.typeVersion)
+
+ val location = this.location.toJSON(objectMapper)
+ node.set("location", location)
+
+ val metadata = objectMapper.createObjectNode()
+ metadata.put("bookChapterProgress", this.bookChapterProgress)
+ metadata.put("bookChapterTitle", this.bookChapterTitle)
+ metadata.put("bookProgress", this.bookProgress)
+ metadata.put("bookTitle", this.bookTitle)
+ metadata.put("deviceID", this.deviceID)
+ metadata.put("kind", this.kind.motivationURI)
+ metadata.put("opdsId", this.opdsId)
+ metadata.put("time", this.dateFormatter.print(this.time))
+ this.uri.let { x -> metadata.put("uri", x.toString()) }
+
+ node.set("metadata", metadata)
+ return node
+ }
+
+ override fun addToDigest(
+ digest: MessageDigest
+ ) {
+ BookmarkDigests.addToDigest(digest, this.opdsId)
+ this.location.addToDigest(digest)
+ digest.update(this.dateFormatter.print(this.time).toByteArray(UTF_8))
+ }
+
+ override fun withURI(uri: URI): SerializedBookmark {
+ return this.copy(uri = uri)
+ }
+
+ override fun withoutURI(): SerializedBookmark {
+ return this.copy(uri = null)
+ }
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarkLegacy.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarkLegacy.kt
new file mode 100644
index 000000000..1662ea596
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarkLegacy.kt
@@ -0,0 +1,111 @@
+package org.nypl.simplified.books.api.bookmark
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import org.joda.time.DateTime
+import org.joda.time.format.ISODateTimeFormat
+import org.nypl.simplified.books.api.BookID
+import org.nypl.simplified.books.api.BookIDs
+import java.net.URI
+import java.nio.charset.StandardCharsets.UTF_8
+import java.security.MessageDigest
+
+data class SerializedBookmarkLegacy(
+ override val bookChapterProgress: Double,
+ override val bookChapterTitle: String,
+ override val bookTitle: String,
+ override val deviceID: String,
+ override val kind: BookmarkKind,
+ override val location: SerializedLocator,
+ override val opdsId: String,
+ override val time: DateTime,
+ override val uri: URI?,
+ override val bookProgress: Double
+) : SerializedBookmark() {
+
+ private val dateFormatter =
+ ISODateTimeFormat.dateTime()
+ .withZoneUTC()
+
+ override val typeName: String
+ get() = "Bookmark"
+
+ override val typeVersion: Int
+ get() = 20210316
+
+ override val book: BookID
+ get() = BookIDs.newFromText(this.opdsId)
+
+ override val bookmarkId: BookmarkID
+ get() = this.createBookmarkId()
+
+ private fun createBookmarkId(): BookmarkID {
+ val digest = MessageDigest.getInstance("SHA-256")
+ this.addToDigest(digest)
+
+ val digestResult = digest.digest()
+ val builder = StringBuilder(64)
+ for (index in digestResult.indices) {
+ val bb = digestResult[index]
+ builder.append(String.format("%02x", bb))
+ }
+ return BookmarkID(builder.toString())
+ }
+
+ override fun toJSON(
+ objectMapper: ObjectMapper
+ ): ObjectNode {
+ val node = objectMapper.createObjectNode()
+ node.put("@type", this.typeName)
+ node.put("@version", this.typeVersion)
+
+ // Legacy bookmarks had these at the top level
+ node.put("bookProgress", this.bookProgress)
+ node.put("chapterTitle", this.bookChapterTitle)
+
+ when (this.location) {
+ is SerializedLocatorAudioBookTime1 -> {
+ node.put(
+ "chapterProgress",
+ this.location.timeMilliseconds.toDouble() / this.location.duration.toDouble()
+ )
+ }
+ is SerializedLocatorHrefProgression20210317 -> {
+ node.put("chapterProgress", this.location.chapterProgress)
+ }
+ is SerializedLocatorLegacyCFI -> {
+ node.put("chapterProgress", this.location.chapterProgression)
+ }
+ is SerializedLocatorPage1 -> {
+ // Nothing
+ }
+ is SerializedLocatorAudioBookTime2 -> {
+ // Nothing
+ }
+ }
+
+ val location = this.location.toJSON(objectMapper)
+ node.set("location", location)
+ node.put("time", dateFormatter.print(this.time))
+ node.put("kind", this.kind.toString())
+ this.deviceID.let { device -> node.put("deviceID", device) }
+ this.uri.let { u -> node.put("uri", u.toString()) }
+ return node
+ }
+
+ override fun addToDigest(
+ digest: MessageDigest
+ ) {
+ BookmarkDigests.addToDigest(digest, this.opdsId)
+ this.location.addToDigest(digest)
+ digest.update(this.dateFormatter.print(this.time).toByteArray(UTF_8))
+ }
+
+ override fun withURI(uri: URI): SerializedBookmark {
+ return this.copy(uri = uri)
+ }
+
+ override fun withoutURI(): SerializedBookmark {
+ return this.copy(uri = null)
+ }
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarks.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarks.kt
new file mode 100644
index 000000000..76d51d10c
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarks.kt
@@ -0,0 +1,238 @@
+package org.nypl.simplified.books.api.bookmark
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import org.joda.time.DateTime
+import org.nypl.simplified.json.core.JSONParseException
+import org.nypl.simplified.json.core.JSONParserUtilities
+import java.net.URI
+
+object SerializedBookmarks {
+
+ private val objectMapper: ObjectMapper =
+ ObjectMapper()
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun parseBookmarkFromString(
+ text: String
+ ): SerializedBookmark {
+ return this.parseBookmark(this.objectMapper.readTree(text))
+ }
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun parseBookmark(
+ node: JsonNode
+ ): SerializedBookmark {
+ val type = node["@type"]
+ return if (type != null) {
+ when (type.asText()) {
+ "Bookmark" -> {
+ this.parseBookmarkGuess(node)
+ }
+ else -> {
+ this.parseBookmarkGuess(node)
+ }
+ }
+ } else {
+ this.parseBookmarkGuess(node)
+ }
+ }
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ private fun parseBookmarkGuess(
+ node: JsonNode
+ ): SerializedBookmark {
+ val version = node["@version"]
+ return if (version != null) {
+ when (version.asText()) {
+ "20210317" -> {
+ this.parseBookmark20210317(node)
+ }
+ "20210828" -> {
+ this.parseBookmark20210828(node)
+ }
+ "20240424" -> {
+ this.parseBookmark20240424(node)
+ }
+ else -> {
+ this.parseBookmarkLegacy(node)
+ }
+ }
+ } else {
+ this.parseBookmarkLegacy(node)
+ }
+ }
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ private fun parseBookmark20240424(
+ node: JsonNode
+ ): SerializedBookmark {
+ return when (node) {
+ is ObjectNode -> {
+ val metadata =
+ JSONParserUtilities.checkObject("metadata", node.get("metadata"))
+ val opdsId =
+ JSONParserUtilities.getString(metadata, "opdsId")
+ val time =
+ JSONParserUtilities.getTimestamp(metadata, "time")
+ val deviceId =
+ JSONParserUtilities.getStringDefault(metadata, "deviceID", "null")
+ val uri =
+ JSONParserUtilities.getURIOrNull(metadata, "uri")
+ val bookChapterTitle =
+ JSONParserUtilities.getStringDefault(metadata, "bookChapterTitle", "")
+ val bookTitle =
+ JSONParserUtilities.getStringDefault(metadata, "bookTitle", "")
+ val bookProgress =
+ JSONParserUtilities.getDoubleDefault(metadata, "bookProgress", 0.0)
+ val bookChapterProgress =
+ JSONParserUtilities.getDoubleDefault(metadata, "bookChapterProgress", 0.0)
+
+ SerializedBookmark20240424(
+ bookChapterProgress = bookChapterProgress,
+ bookChapterTitle = bookChapterTitle,
+ bookProgress = bookProgress,
+ bookTitle = bookTitle,
+ deviceID = deviceId,
+ kind = BookmarkKind.BookmarkExplicit,
+ location = SerializedLocators.parseLocator(node.get("location")),
+ opdsId = opdsId,
+ time = time,
+ uri = uri,
+ )
+ }
+ else -> {
+ throw JSONParseException(
+ String.format(
+ "Bookmarks can only be parsed from JSON object nodes (received %s)",
+ node.nodeType
+ )
+ )
+ }
+ }
+ }
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ private fun parseBookmarkLegacy(
+ node: JsonNode
+ ): SerializedBookmarkLegacy {
+ return when (node) {
+ is ObjectNode -> {
+ SerializedBookmarkLegacy(
+ bookChapterProgress = 0.0,
+ bookChapterTitle = JSONParserUtilities.getStringDefault(node, "chapterTitle", ""),
+ bookProgress = JSONParserUtilities.getDoubleDefault(node, "bookProgress", 0.0),
+ bookTitle = "",
+ deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", "null"),
+ kind = BookmarkKind.BookmarkExplicit,
+ location = SerializedLocators.parseLocator(node.get("location")),
+ opdsId = JSONParserUtilities.getString(node, "opdsId"),
+ time = JSONParserUtilities.getTimestamp(node, "time"),
+ uri = JSONParserUtilities.getURIOrNull(node, "uri"),
+ )
+ }
+ else -> {
+ throw JSONParseException(
+ String.format(
+ "Bookmarks can only be parsed from JSON object nodes (received %s)",
+ node.nodeType
+ )
+ )
+ }
+ }
+ }
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ private fun parseBookmark20210317(
+ node: JsonNode
+ ): SerializedBookmark20210317 {
+ return when (node) {
+ is ObjectNode -> {
+ SerializedBookmark20210317(
+ bookChapterProgress = 0.0,
+ bookChapterTitle = JSONParserUtilities.getStringDefault(node, "chapterTitle", ""),
+ bookProgress = JSONParserUtilities.getDoubleDefault(node, "bookProgress", 0.0),
+ bookTitle = "",
+ deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", "null"),
+ kind = BookmarkKind.BookmarkExplicit,
+ location = SerializedLocators.parseLocator(node.get("location")),
+ opdsId = JSONParserUtilities.getString(node, "opdsId"),
+ time = JSONParserUtilities.getTimestamp(node, "time"),
+ uri = JSONParserUtilities.getURIOrNull(node, "uri"),
+ )
+ }
+ else -> {
+ throw JSONParseException(
+ String.format(
+ "Bookmarks can only be parsed from JSON object nodes (received %s)",
+ node.nodeType
+ )
+ )
+ }
+ }
+ }
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ private fun parseBookmark20210828(
+ node: JsonNode
+ ): SerializedBookmark20210828 {
+ return when (node) {
+ is ObjectNode -> {
+ SerializedBookmark20210828(
+ bookChapterProgress = 0.0,
+ bookChapterTitle = JSONParserUtilities.getStringDefault(node, "chapterTitle", ""),
+ bookProgress = JSONParserUtilities.getDoubleDefault(node, "bookProgress", 0.0),
+ bookTitle = "",
+ deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", "null"),
+ kind = BookmarkKind.BookmarkExplicit,
+ location = SerializedLocators.parseLocator(node.get("location")),
+ opdsId = JSONParserUtilities.getString(node, "opdsId"),
+ time = JSONParserUtilities.getTimestamp(node, "time"),
+ uri = JSONParserUtilities.getURIOrNull(node, "uri"),
+ )
+ }
+ else -> {
+ throw JSONParseException(
+ String.format(
+ "Bookmarks can only be parsed from JSON object nodes (received %s)",
+ node.nodeType
+ )
+ )
+ }
+ }
+ }
+
+ fun createWithCurrentFormat(
+ bookChapterProgress: Double,
+ bookChapterTitle: String,
+ bookProgress: Double,
+ bookTitle: String,
+ deviceID: String,
+ kind: BookmarkKind,
+ location: SerializedLocator,
+ opdsId: String,
+ time: DateTime,
+ uri: URI?
+ ): SerializedBookmark {
+ return SerializedBookmark20240424(
+ bookChapterProgress = bookChapterProgress,
+ bookChapterTitle = bookChapterTitle,
+ bookProgress = bookProgress,
+ bookTitle = bookTitle,
+ deviceID = deviceID,
+ kind = kind,
+ location = location,
+ opdsId = opdsId,
+ time = time,
+ uri = uri
+ )
+ }
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocator.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocator.kt
new file mode 100644
index 000000000..124f1dc73
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocator.kt
@@ -0,0 +1,44 @@
+package org.nypl.simplified.books.api.bookmark
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import java.security.MessageDigest
+
+/**
+ * @see "https://github.com/ThePalaceProject/mobile-specs/tree/main/bookmarks#locators"
+ */
+
+sealed class SerializedLocator {
+
+ /**
+ * The type name (such as "BookLocationR2")
+ */
+
+ abstract val typeName: String
+
+ /**
+ * The type version (such as 20210317)
+ */
+
+ abstract val typeVersion: Int
+
+ /**
+ * Serialize this locator to JSON.
+ */
+
+ abstract fun toJSON(objectMapper: ObjectMapper): ObjectNode
+
+ /**
+ * Serialize this locator to a JSON string.
+ */
+
+ fun toJSONString(objectMapper: ObjectMapper): String {
+ return objectMapper.writeValueAsString(toJSON(objectMapper))
+ }
+
+ /**
+ * Add the fields of this object to the given message digest
+ */
+
+ abstract fun addToDigest(digest: MessageDigest)
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime1.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime1.kt
new file mode 100644
index 000000000..f86d66b8c
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime1.kt
@@ -0,0 +1,61 @@
+package org.nypl.simplified.books.api.bookmark
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import java.nio.charset.StandardCharsets.UTF_8
+import java.security.MessageDigest
+
+/**
+ * The version 1 standardization of "audio book time" locators.
+ *
+ * @see "https://github.com/ThePalaceProject/mobile-specs/tree/main/bookmarks#locatoraudiobooktime"
+ */
+
+data class SerializedLocatorAudioBookTime1(
+ val audioBookId: String,
+ val chapter: Int,
+ val duration: Long,
+ val part: Int,
+ val startOffsetMilliseconds: Long,
+ val timeMilliseconds: Long,
+ val title: String,
+) : SerializedLocator() {
+
+ val timeWithoutOffset: Long
+ get() = this.timeMilliseconds - this.startOffsetMilliseconds
+
+ override val typeName: String
+ get() = "LocatorAudioBookTime"
+
+ override val typeVersion: Int
+ get() = 1
+
+ override fun toJSON(
+ objectMapper: ObjectMapper
+ ): ObjectNode {
+ val root = objectMapper.createObjectNode()
+ root.put("@type", this.typeName)
+ root.put("@version", this.typeVersion)
+
+ root.put("audiobookID", this.audioBookId)
+ root.put("chapter", this.chapter)
+ root.put("duration", this.duration)
+ root.put("part", this.part)
+ root.put("startOffset", this.startOffsetMilliseconds)
+ root.put("time", this.timeMilliseconds)
+ root.put("title", this.title)
+ return root
+ }
+
+ override fun addToDigest(
+ digest: MessageDigest
+ ) {
+ digest.update(this.audioBookId.toByteArray(UTF_8))
+ digest.update(this.chapter.toString().toByteArray(UTF_8))
+ digest.update(this.duration.toString().toByteArray(UTF_8))
+ digest.update(this.part.toString().toByteArray(UTF_8))
+ digest.update(this.startOffsetMilliseconds.toString().toByteArray(UTF_8))
+ digest.update(this.timeMilliseconds.toString().toByteArray(UTF_8))
+ digest.update(this.title.toByteArray(UTF_8))
+ }
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime2.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime2.kt
new file mode 100644
index 000000000..3ad559f33
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime2.kt
@@ -0,0 +1,43 @@
+package org.nypl.simplified.books.api.bookmark
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import java.nio.charset.StandardCharsets.UTF_8
+import java.security.MessageDigest
+
+/**
+ * The version 2 standardization of "audio book time" locators.
+ *
+ * @see "https://github.com/ThePalaceProject/mobile-specs/tree/main/bookmarks#locatoraudiobooktime"
+ */
+
+data class SerializedLocatorAudioBookTime2(
+ val chapterHref: String,
+ val chapterOffsetMilliseconds: Long
+) : SerializedLocator() {
+
+ override val typeName: String
+ get() = "LocatorAudioBookTime"
+
+ override val typeVersion: Int
+ get() = 2
+
+ override fun toJSON(
+ objectMapper: ObjectMapper
+ ): ObjectNode {
+ val root = objectMapper.createObjectNode()
+ root.put("@type", this.typeName)
+ root.put("@version", this.typeVersion)
+
+ root.put("chapterHref", this.chapterHref)
+ root.put("chapterOffsetMilliseconds", this.chapterOffsetMilliseconds)
+ return root
+ }
+
+ override fun addToDigest(
+ digest: MessageDigest
+ ) {
+ digest.update(this.chapterHref.toByteArray(UTF_8))
+ digest.update(this.chapterOffsetMilliseconds.toString().toByteArray(UTF_8))
+ }
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorHrefProgression20210317.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorHrefProgression20210317.kt
new file mode 100644
index 000000000..15ede57e6
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorHrefProgression20210317.kt
@@ -0,0 +1,51 @@
+package org.nypl.simplified.books.api.bookmark
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import java.nio.charset.StandardCharsets.UTF_8
+import java.security.MessageDigest
+
+/**
+ * The 20210317 standardization of R2 "href/progression" locators.
+ *
+ * @see "https://github.com/ThePalaceProject/mobile-specs/tree/main/bookmarks#locatorhrefprogression"
+ */
+
+data class SerializedLocatorHrefProgression20210317(
+ /**
+ * The href of the chapter.
+ */
+
+ val chapterHref: String,
+
+ /**
+ * The progress through the chapter.
+ */
+
+ val chapterProgress: Double,
+) : SerializedLocator() {
+
+ override val typeName: String
+ get() = "LocatorHrefProgression"
+
+ override val typeVersion: Int
+ get() = 20210317
+
+ override fun toJSON(
+ objectMapper: ObjectMapper
+ ): ObjectNode {
+ val root = objectMapper.createObjectNode()
+ root.put("@type", this.typeName)
+ root.put("@version", this.typeVersion)
+ root.put("href", this.chapterHref)
+ root.put("progressWithinChapter", this.chapterProgress)
+ return root
+ }
+
+ override fun addToDigest(
+ digest: MessageDigest
+ ) {
+ digest.update(this.chapterHref.toByteArray(UTF_8))
+ digest.update(String.format("%.6f", this.chapterProgress).toByteArray(UTF_8))
+ }
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorLegacyCFI.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorLegacyCFI.kt
new file mode 100644
index 000000000..4fcc4ba52
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorLegacyCFI.kt
@@ -0,0 +1,47 @@
+package org.nypl.simplified.books.api.bookmark
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import java.nio.charset.StandardCharsets.UTF_8
+import java.security.MessageDigest
+
+/**
+ * The version 1 standardization of legacy "CFI" locators.
+ *
+ * @see "https://github.com/ThePalaceProject/mobile-specs/tree/main/bookmarks#locatorlegacycfi"
+ */
+
+data class SerializedLocatorLegacyCFI(
+ val idRef: String?,
+ val contentCFI: String?,
+ val chapterProgression: Double
+) : SerializedLocator() {
+
+ override val typeName: String
+ get() = "LocatorLegacyCFI"
+
+ override val typeVersion: Int
+ get() = 1
+
+ override fun toJSON(
+ objectMapper: ObjectMapper
+ ): ObjectNode {
+ val root = objectMapper.createObjectNode()
+ root.put("@type", this.typeName)
+ root.put("@version", this.typeVersion)
+
+ this.idRef?.let { x -> root.put("idref", x) }
+ this.contentCFI?.let { x -> root.put("contentCFI", x) }
+
+ root.put("progressWithinChapter", this.chapterProgression)
+ return root
+ }
+
+ override fun addToDigest(
+ digest: MessageDigest
+ ) {
+ this.idRef?.let { x -> digest.update(x.toByteArray(UTF_8)) }
+ this.contentCFI?.let { x -> digest.update(x.toByteArray(UTF_8)) }
+ digest.update(String.format("%.6f", this.chapterProgression).toByteArray(UTF_8))
+ }
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorPage1.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorPage1.kt
new file mode 100644
index 000000000..339f82f63
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorPage1.kt
@@ -0,0 +1,43 @@
+package org.nypl.simplified.books.api.bookmark
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import java.nio.charset.StandardCharsets.UTF_8
+import java.security.MessageDigest
+
+/**
+ * The version 1 standardization of "page" locators.
+ *
+ * @see "https://github.com/ThePalaceProject/mobile-specs/tree/main/bookmarks#locatorpage"
+ */
+
+data class SerializedLocatorPage1(
+ /**
+ * The page number.
+ */
+
+ val page: Int,
+) : SerializedLocator() {
+
+ override val typeName: String
+ get() = "LocatorPage"
+
+ override val typeVersion: Int
+ get() = 1
+
+ override fun toJSON(
+ objectMapper: ObjectMapper
+ ): ObjectNode {
+ val root = objectMapper.createObjectNode()
+ root.put("@type", this.typeName)
+ root.put("@version", this.typeVersion)
+ root.put("page", this.page)
+ return root
+ }
+
+ override fun addToDigest(
+ digest: MessageDigest
+ ) {
+ digest.update(this.page.toString().toByteArray(UTF_8))
+ }
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocators.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocators.kt
new file mode 100644
index 000000000..51d72bb42
--- /dev/null
+++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocators.kt
@@ -0,0 +1,169 @@
+package org.nypl.simplified.books.api.bookmark
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import org.nypl.simplified.json.core.JSONParseException
+import org.nypl.simplified.json.core.JSONParserUtilities
+import java.math.BigInteger
+
+object SerializedLocators {
+
+ private val objectMapper: ObjectMapper =
+ ObjectMapper()
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun parseLocatorFromString(
+ text: String
+ ): SerializedLocator {
+ return this.parseLocator(this.objectMapper.readTree(text))
+ }
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun parseLocator(
+ node: JsonNode
+ ): SerializedLocator {
+ return when (node) {
+ is ObjectNode -> {
+ this.parseLocator(node)
+ }
+
+ else -> {
+ throw JSONParseException(
+ String.format(
+ "Locators can only be parsed from JSON object nodes (received %s)",
+ node.nodeType
+ )
+ )
+ }
+ }
+ }
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun parseLocator(
+ node: ObjectNode
+ ): SerializedLocator {
+ val type = node["@type"]
+ return if (type != null) {
+ when (type.asText()) {
+ "BookLocationR1", "LocatorLegacyCFI" -> {
+ this.parseLocatorLegacyCFI(node)
+ }
+
+ "LocatorAudioBookTime" -> {
+ this.parseLocatorAudioBookTime(node)
+ }
+
+ "BookLocationR2", "LocatorHrefProgression" -> {
+ this.parseLocatorHrefProgression(node)
+ }
+
+ "LocatorPage" -> {
+ this.parseLocatorPage(node)
+ }
+
+ else -> {
+ this.parseLocatorGuess(node)
+ }
+ }
+ } else {
+ this.parseLocatorGuess(node)
+ }
+ }
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ private fun parseLocatorLegacyCFI(
+ node: ObjectNode
+ ): SerializedLocatorLegacyCFI {
+ return SerializedLocatorLegacyCFI(
+ idRef = JSONParserUtilities.getStringOrNull(node, "idref"),
+ contentCFI = JSONParserUtilities.getStringOrNull(node, "contentCFI"),
+ chapterProgression = JSONParserUtilities.getDoubleDefault(node, "progressWithinChapter", 0.0)
+ )
+ }
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ private fun parseLocatorGuess(
+ node: ObjectNode
+ ): SerializedLocator {
+ return this.parseLocatorLegacyCFI(node)
+ }
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ private fun parseLocatorPage(
+ node: ObjectNode
+ ): SerializedLocatorPage1 {
+ return SerializedLocatorPage1(
+ page = JSONParserUtilities.getInteger(node, "page"),
+ )
+ }
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ private fun parseLocatorHrefProgression(
+ node: ObjectNode
+ ): SerializedLocatorHrefProgression20210317 {
+ return SerializedLocatorHrefProgression20210317(
+ chapterHref = JSONParserUtilities.getString(node, "href"),
+ chapterProgress = JSONParserUtilities.getDouble(node, "progressWithinChapter")
+ )
+ }
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ private fun parseLocatorAudioBookTime(
+ node: ObjectNode
+ ): SerializedLocator {
+ return when (val version = JSONParserUtilities.getIntegerDefault(node, "@version", 1)) {
+ 1 -> this.parseLocatorAudioBookTime1(node)
+ 2 -> this.parseLocatorAudioBookTime2(node)
+
+ else -> {
+ throw JSONParseException(
+ String.format(
+ "Unsupported audio book locator version (received %s)",
+ version
+ )
+ )
+ }
+ }
+ }
+
+ private fun parseLocatorAudioBookTime2(
+ node: ObjectNode
+ ): SerializedLocatorAudioBookTime2 {
+ return SerializedLocatorAudioBookTime2(
+ chapterHref =
+ JSONParserUtilities.getString(node, "chapterHref"),
+ chapterOffsetMilliseconds =
+ JSONParserUtilities.getBigInteger(node, "chapterOffsetMilliseconds").toLong()
+ )
+ }
+
+ private fun parseLocatorAudioBookTime1(
+ node: ObjectNode
+ ): SerializedLocatorAudioBookTime1 {
+ val startOffset =
+ JSONParserUtilities.getBigIntegerDefault(node, "startOffset", BigInteger.ZERO)
+ .toLong()
+ val timeMillisecondsRaw =
+ JSONParserUtilities.getBigInteger(node, "time")
+ .toLong()
+
+ return SerializedLocatorAudioBookTime1(
+ part = JSONParserUtilities.getInteger(node, "part"),
+ chapter = JSONParserUtilities.getInteger(node, "chapter"),
+ title = JSONParserUtilities.getString(node, "title"),
+ audioBookId = JSONParserUtilities.getString(node, "audiobookID"),
+ duration = JSONParserUtilities.getBigInteger(node, "duration").toLong(),
+ startOffsetMilliseconds = startOffset,
+ timeMilliseconds = timeMillisecondsRaw,
+ )
+ }
+}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/AudiobookLocationJSON.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/AudiobookLocationJSON.kt
deleted file mode 100644
index 7f6960bb7..000000000
--- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/AudiobookLocationJSON.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-package org.nypl.simplified.books.api.helper
-
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.databind.node.ObjectNode
-import org.librarysimplified.audiobook.api.PlayerPosition
-import org.nypl.simplified.json.core.JSONParseException
-import org.nypl.simplified.json.core.JSONParserUtilities
-import org.nypl.simplified.json.core.JSONSerializerUtilities
-import java.io.ByteArrayOutputStream
-import java.io.IOException
-
-/**
- * Functions to serialize audiobook locations to/from JSON.
- */
-
-object AudiobookLocationJSON {
-
- /**
- * Deserialize audiobook player positions from the given JSON node.
- *
- * @param node A JSON node
- * @return A parsed player position
- * @throws JSONParseException On parse errors
- */
-
- @Throws(JSONParseException::class)
- fun deserializeFromJSON(
- node: JsonNode
- ): PlayerPosition {
- val obj =
- JSONParserUtilities.checkObject(null, node)
- return PlayerPosition(
- title = JSONParserUtilities.getStringOrNull(obj, "title"),
- part = JSONParserUtilities.getIntegerDefault(obj, "part", 0),
- chapter = JSONParserUtilities.getIntegerDefault(obj, "chapter", 0),
- startOffset = JSONParserUtilities.getIntegerDefault(obj, "startOffset", 0).toLong(),
- currentOffset = JSONParserUtilities.getIntegerDefault(obj, "time", 0).toLong()
- )
- }
-
- /**
- * Serialize reader book locations to JSON.
- *
- * @param objectMapper A JSON object mapper
- * @param position The position of the audiobook
- * @return A serialized object
- */
-
- fun serializeToJSON(
- objectMapper: ObjectMapper,
- position: PlayerPosition
- ): ObjectNode {
- val root = objectMapper.createObjectNode()
- root.put("chapter", position.chapter)
- root.put("startOffset", position.startOffset)
- root.put("time", position.currentOffset)
- root.put("part", position.part)
- root.put("title", position.title)
- return root
- }
-
- /**
- * Serialize reader book locations to a JSON string.
- *
- * @param objectMapper A JSON object mapper
- * @param position The position in the audiobook
- * @return A JSON string
- * @throws IOException On serialization errors
- */
-
- @Throws(IOException::class)
- fun serializeToString(
- objectMapper: ObjectMapper,
- position: PlayerPosition
- ): String {
- val jo = serializeToJSON(objectMapper, position)
- val bao = ByteArrayOutputStream(1024)
- JSONSerializerUtilities.serialize(jo, bao)
- return bao.toString("UTF-8")
- }
-
- /**
- * Deserialize a reader book location from the given string.
- *
- * @param objectMapper A JSON object mapper
- * @param text The text to map
- * @return A parsed player position
- * @throws IOException On I/O or parser errors
- */
-
- @Throws(IOException::class)
- fun deserializeFromString(
- objectMapper: ObjectMapper,
- text: String
- ): PlayerPosition {
- val node = objectMapper.readTree(text)
- return deserializeFromJSON(
- node = JSONParserUtilities.checkObject(null, node)
- )
- }
-}
diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/ReaderLocationJSON.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/ReaderLocationJSON.kt
deleted file mode 100644
index f6c6ab0c6..000000000
--- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/ReaderLocationJSON.kt
+++ /dev/null
@@ -1,224 +0,0 @@
-package org.nypl.simplified.books.api.helper
-
-import com.fasterxml.jackson.databind.JsonNode
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.databind.node.ObjectNode
-import org.nypl.simplified.books.api.BookChapterProgress
-import org.nypl.simplified.books.api.BookLocation
-import org.nypl.simplified.books.api.BookLocation.BookLocationR1
-import org.nypl.simplified.books.api.BookLocation.BookLocationR2
-import org.nypl.simplified.json.core.JSONParseException
-import org.nypl.simplified.json.core.JSONParserUtilities
-import org.nypl.simplified.json.core.JSONSerializerUtilities
-import java.io.ByteArrayOutputStream
-import java.io.IOException
-
-/**
- * Functions to serialize and reader book locations to/from JSON.
- */
-
-object ReaderLocationJSON {
-
- /**
- * Deserialize chapter progress from the given JSON node.
- *
- * @param node A JSON node
- * @return A parsed description
- * @throws JSONParseException On parse errors
- */
-
- @Throws(JSONParseException::class)
- fun deserializeProgressFromJSON(
- node: JsonNode
- ): BookChapterProgress {
- val obj = JSONParserUtilities.checkObject(null, node)
- return BookChapterProgress(
- chapterHref = JSONParserUtilities.getString(obj, "chapterHref"),
- chapterProgress = JSONParserUtilities.getDouble(obj, "chapterProgress")
- )
- }
-
- /**
- * Deserialize reader book locations from the given JSON node.
- *
- * @param node A JSON node
- * @return A book location
- * @throws JSONParseException On parse errors
- */
-
- @Throws(JSONParseException::class)
- fun deserializeFromJSON(
- node: JsonNode
- ): BookLocation {
- val obj =
- JSONParserUtilities.checkObject(null, node)
- val type =
- JSONParserUtilities.getStringOrNull(obj, "@type")
- ?: return deserializeFromJSONR1Old(obj)
- return when (type) {
- "BookLocationR2" ->
- deserializeFromJSONR2(obj)
- "BookLocationR1" ->
- deserializeFromJSONR1(obj)
- else ->
- throw JSONParseException("Unsupported location type: $type")
- }
- }
-
- private fun deserializeFromJSONR1(
- obj: ObjectNode
- ): BookLocationR1 {
- val version =
- JSONParserUtilities.getIntegerOrNull(obj, "@version")
- ?: return deserializeFromJSONR1Old(obj)
- return when (version) {
- 20210317 ->
- deserializeFromJSONR1_20210317(obj)
- else ->
- throw JSONParseException("Unsupported book location format version: $version")
- }
- }
-
- private fun deserializeFromJSONR1Old(
- obj: ObjectNode
- ): BookLocationR1 {
- return BookLocationR1(
- progress = JSONParserUtilities.getDoubleDefault(obj, "chapterProgress", 0.0),
- contentCFI = JSONParserUtilities.getStringOrNull(obj, "contentCFI"),
- idRef = JSONParserUtilities.getStringOrNull(obj, "idref")
- )
- }
-
- private fun deserializeFromJSONR1_20210317(
- obj: ObjectNode
- ): BookLocationR1 {
- return BookLocationR1(
- progress = JSONParserUtilities.getDouble(obj, "chapterProgress"),
- contentCFI = JSONParserUtilities.getStringOrNull(obj, "contentCFI"),
- idRef = JSONParserUtilities.getStringOrNull(obj, "idref")
- )
- }
-
- @Throws(JSONParseException::class)
- private fun deserializeFromJSONR2(
- obj: ObjectNode
- ): BookLocation {
- val version =
- JSONParserUtilities.getIntegerOrNull(obj, "@version")
- ?: return deserializeFromJSONR2Old()
- return when (version) {
- 20210317 ->
- deserializeFromJSONR2_20210317(obj)
- else ->
- throw JSONParseException("Unsupported book location format version: $version")
- }
- }
-
- @Throws(JSONParseException::class)
- private fun deserializeFromJSONR2_20210317(
- obj: ObjectNode
- ): BookLocationR2 {
- val progress = JSONParserUtilities.getObject(obj, "progress")
- return BookLocationR2(deserializeProgressFromJSON(progress))
- }
-
- @Throws(JSONParseException::class)
- private fun deserializeFromJSONR2Old(): BookLocation {
- throw JSONParseException("Unsupported book location format version: (unspecified)")
- }
-
- /**
- * Serialize reader book locations to JSON.
- *
- * @param objectMapper A JSON object mapper
- * @return A serialized object
- */
-
- fun serializeToJSON(
- objectMapper: ObjectMapper,
- description: BookLocation
- ): ObjectNode {
- return when (description) {
- is BookLocationR2 ->
- serializeToJSONR2(objectMapper, description)
- is BookLocationR1 ->
- serializeToJSONR1(objectMapper, description)
- }
- }
-
- private fun serializeToJSONR2(
- objectMapper: ObjectMapper,
- description: BookLocationR2
- ): ObjectNode {
- val root = objectMapper.createObjectNode()
- root.put("@type", "BookLocationR2")
- root.put("@version", 20210317)
-
- val progress = objectMapper.createObjectNode()
- progress.put("chapterHref", description.progress.chapterHref)
- progress.put("chapterProgress", description.progress.chapterProgress)
-
- root.set("progress", progress)
- return root
- }
-
- private fun serializeToJSONR1(
- objectMapper: ObjectMapper,
- description: BookLocationR1
- ): ObjectNode {
- val root = objectMapper.createObjectNode()
-
- root.put("@type", "BookLocationR1")
- root.put("@version", 20210317)
-
- val contentCFI = description.contentCFI
- if (contentCFI != null) {
- root.put("contentCFI", contentCFI)
- }
- val idRef = description.idRef
- if (idRef != null) {
- root.put("idref", idRef)
- }
-
- root.put("chapterProgress", description.progress ?: 0.0)
- return root
- }
-
- /**
- * Serialize reader book locations to a JSON string.
- *
- * @param objectMapper A JSON object mapper
- * @return A JSON string
- * @throws IOException On serialization errors
- */
-
- @Throws(IOException::class)
- fun serializeToString(
- objectMapper: ObjectMapper,
- description: BookLocation
- ): String {
- val jo = serializeToJSON(objectMapper, description)
- val bao = ByteArrayOutputStream(1024)
- JSONSerializerUtilities.serialize(jo, bao)
- return bao.toString("UTF-8")
- }
-
- /**
- * Deserialize a reader book location from the given string.
- *
- * @param objectMapper A JSON object mapper
- * @return A parsed location
- * @throws IOException On I/O or parser errors
- */
-
- @Throws(IOException::class)
- fun deserializeFromString(
- objectMapper: ObjectMapper,
- text: String
- ): BookLocation {
- val node = objectMapper.readTree(text)
- return deserializeFromJSON(
- node = JSONParserUtilities.checkObject(null, node)
- )
- }
-}
diff --git a/simplified-books-audio/build.gradle.kts b/simplified-books-audio/build.gradle.kts
index ea39e15ad..5ce2944ce 100644
--- a/simplified-books-audio/build.gradle.kts
+++ b/simplified-books-audio/build.gradle.kts
@@ -26,6 +26,6 @@ dependencies {
implementation(libs.palace.audiobook.parser.api)
implementation(libs.palace.http.api)
implementation(libs.r2.shared)
- implementation(libs.rxjava)
+ implementation(libs.rxjava2)
implementation(libs.slf4j)
}
diff --git a/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AbstractAudioBookManifestStrategy.kt b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AbstractAudioBookManifestStrategy.kt
index 855f6d3e1..3b2a6be6c 100644
--- a/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AbstractAudioBookManifestStrategy.kt
+++ b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AbstractAudioBookManifestStrategy.kt
@@ -1,6 +1,8 @@
package org.nypl.simplified.books.audio
import android.app.Application
+import io.reactivex.Observable
+import io.reactivex.subjects.PublishSubject
import one.irradia.mime.api.MIMEType
import org.librarysimplified.audiobook.api.PlayerResult
import org.librarysimplified.audiobook.license_check.api.LicenseCheckParameters
@@ -19,8 +21,6 @@ import org.nypl.simplified.taskrecorder.api.TaskRecorder
import org.nypl.simplified.taskrecorder.api.TaskRecorderType
import org.nypl.simplified.taskrecorder.api.TaskResult
import org.slf4j.LoggerFactory
-import rx.Observable
-import rx.subjects.PublishSubject
import java.net.URI
/**
@@ -271,7 +271,7 @@ abstract class AbstractAudioBookManifestStrategy(
try {
return check.execute()
} finally {
- checkSubscription.unsubscribe()
+ checkSubscription.dispose()
}
}
diff --git a/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestStrategyType.kt b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestStrategyType.kt
index 2d1f11a12..5e6b37ca2 100644
--- a/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestStrategyType.kt
+++ b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestStrategyType.kt
@@ -1,7 +1,7 @@
package org.nypl.simplified.books.audio
+import io.reactivex.Observable
import org.nypl.simplified.taskrecorder.api.TaskResult
-import rx.Observable
/**
* A strategy for obtaining, parsing, and license-checking an audio book manifest. A given
diff --git a/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/UnpackagedAudioBookManifestStrategy.kt b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/UnpackagedAudioBookManifestStrategy.kt
index 351b35f6b..2b9b6961e 100644
--- a/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/UnpackagedAudioBookManifestStrategy.kt
+++ b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/UnpackagedAudioBookManifestStrategy.kt
@@ -91,7 +91,7 @@ class UnpackagedAudioBookManifestStrategy(
try {
return strategy.execute()
} finally {
- fulfillSubscription.unsubscribe()
+ fulfillSubscription.dispose()
}
}
diff --git a/simplified-books-borrowing/build.gradle.kts b/simplified-books-borrowing/build.gradle.kts
index 277b1d89e..304e7aeb1 100644
--- a/simplified-books-borrowing/build.gradle.kts
+++ b/simplified-books-borrowing/build.gradle.kts
@@ -24,9 +24,9 @@ dependencies {
implementation(libs.io7m.junreachable)
implementation(libs.irradia.mime.api)
implementation(libs.joda.time)
- implementation(libs.kotlinx.coroutines)
implementation(libs.kotlin.reflect)
implementation(libs.kotlin.stdlib)
+ implementation(libs.kotlinx.coroutines)
implementation(libs.palace.audiobook.api)
implementation(libs.palace.audiobook.manifest.fulfill.api)
implementation(libs.palace.audiobook.manifest.fulfill.spi)
@@ -37,6 +37,7 @@ dependencies {
implementation(libs.r2.lcp)
implementation(libs.r2.shared)
implementation(libs.rxjava)
+ implementation(libs.rxjava2)
implementation(libs.service.wight.core)
implementation(libs.slf4j)
implementation(libs.truecommons.key.disable)
diff --git a/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowACSM.kt b/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowACSM.kt
index f966ef026..3f31cc9c4 100644
--- a/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowACSM.kt
+++ b/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowACSM.kt
@@ -18,8 +18,8 @@ import org.nypl.simplified.adobe.extensions.AdobeDRMExtensions
import org.nypl.simplified.adobe.extensions.AdobeDRMExtensions.AdobeDRMFulfillmentException
import org.nypl.simplified.books.api.BookDRMKind.ACS
import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle.ACSHandle
-import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle.LCPHandle
import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle.AxisHandle
+import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle.LCPHandle
import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle.NoneHandle
import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle
import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook
diff --git a/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowAudioBook.kt b/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowAudioBook.kt
index bdab7fa12..4ba7cbefb 100644
--- a/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowAudioBook.kt
+++ b/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowAudioBook.kt
@@ -172,7 +172,7 @@ class BorrowAudioBook private constructor() : BorrowSubtaskType {
}
}
} finally {
- subscription.unsubscribe()
+ subscription.dispose()
}
}
diff --git a/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/BookSyncTask.kt b/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/BookSyncTask.kt
index dfd02c8b6..b4d8f5835 100644
--- a/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/BookSyncTask.kt
+++ b/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/BookSyncTask.kt
@@ -39,7 +39,6 @@ import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.net.URI
-import java.util.HashSet
import java.util.concurrent.TimeUnit
class BookSyncTask(
diff --git a/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/Controller.kt b/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/Controller.kt
index daf28fc79..32f974285 100644
--- a/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/Controller.kt
+++ b/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/Controller.kt
@@ -24,9 +24,6 @@ import org.nypl.simplified.accounts.api.AccountLogoutStringResourcesType
import org.nypl.simplified.accounts.api.AccountProviderType
import org.nypl.simplified.accounts.database.api.AccountType
import org.nypl.simplified.accounts.database.api.AccountsDatabaseNonexistentException
-import org.nypl.simplified.deeplinks.controller.api.DeepLinkEvent
-import org.nypl.simplified.deeplinks.controller.api.DeepLinksControllerType
-import org.nypl.simplified.deeplinks.controller.api.ScreenID
import org.nypl.simplified.accounts.registry.api.AccountProviderRegistryEvent
import org.nypl.simplified.accounts.registry.api.AccountProviderRegistryType
import org.nypl.simplified.analytics.api.AnalyticsType
@@ -47,6 +44,9 @@ import org.nypl.simplified.books.formats.api.BookFormatSupportType
import org.nypl.simplified.books.preview.BookPreviewRequirements
import org.nypl.simplified.books.preview.BookPreviewTask
import org.nypl.simplified.crashlytics.api.CrashlyticsServiceType
+import org.nypl.simplified.deeplinks.controller.api.DeepLinkEvent
+import org.nypl.simplified.deeplinks.controller.api.DeepLinksControllerType
+import org.nypl.simplified.deeplinks.controller.api.ScreenID
import org.nypl.simplified.feeds.api.Feed
import org.nypl.simplified.feeds.api.FeedEntry
import org.nypl.simplified.feeds.api.FeedLoaderType
diff --git a/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/ProfileFeedTask.kt b/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/ProfileFeedTask.kt
index 2f3cef231..f87b963f6 100644
--- a/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/ProfileFeedTask.kt
+++ b/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/ProfileFeedTask.kt
@@ -17,7 +17,6 @@ import org.nypl.simplified.feeds.api.FeedSearch
import org.nypl.simplified.profiles.controller.api.ProfileFeedRequest
import org.nypl.simplified.profiles.controller.api.ProfilesControllerType
import org.slf4j.LoggerFactory
-import java.util.ArrayList
import java.util.Collections
import java.util.Locale
import java.util.concurrent.Callable
diff --git a/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDRMInformationHandle.kt b/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDRMInformationHandle.kt
index 292e512ef..94fef2e8f 100644
--- a/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDRMInformationHandle.kt
+++ b/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDRMInformationHandle.kt
@@ -1,8 +1,8 @@
package org.nypl.simplified.books.book_database.api
import net.jcip.annotations.ThreadSafe
-import org.nypl.simplified.books.api.BookDRMInformation
import org.nypl.drm.core.AdobeAdeptLoan
+import org.nypl.simplified.books.api.BookDRMInformation
import java.io.File
import java.io.IOException
diff --git a/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDatabaseEntryType.kt b/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDatabaseEntryType.kt
index f6e176b92..b2e16924f 100644
--- a/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDatabaseEntryType.kt
+++ b/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDatabaseEntryType.kt
@@ -8,7 +8,8 @@ import org.nypl.simplified.books.api.BookFormat
import org.nypl.simplified.books.api.BookFormat.BookFormatAudioBook
import org.nypl.simplified.books.api.BookFormat.BookFormatEPUB
import org.nypl.simplified.books.api.BookFormat.BookFormatPDF
-import org.nypl.simplified.books.api.bookmark.Bookmark
+import org.nypl.simplified.books.api.bookmark.BookmarkID
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook
import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB
import org.nypl.simplified.books.book_database.api.BookFormats.BookFormatDefinition.BOOK_FORMAT_AUDIO
@@ -168,6 +169,35 @@ sealed class BookDatabaseEntryFormatHandle {
@Throws(IOException::class)
abstract fun deleteBookData()
+ /**
+ * Delete the bookmark with the given ID.
+ */
+
+ @Throws(IOException::class)
+ abstract fun deleteBookmark(bookmarkId: BookmarkID)
+
+ /**
+ * Set the last read location for the book.
+ *
+ * @param bookmark The location
+ *
+ * @throws IOException On I/O errors
+ */
+
+ @Throws(IOException::class)
+ abstract fun setLastReadLocation(bookmark: SerializedBookmark?)
+
+ /**
+ * Add a bookmark to the book, replacing any existing bookmark with the same ID.
+ *
+ * @param bookmark The location
+ *
+ * @throws IOException On I/O errors
+ */
+
+ @Throws(IOException::class)
+ abstract fun addBookmark(bookmark: SerializedBookmark)
+
/**
* The interface exposed by the EPUB format in database entries.
*/
@@ -189,28 +219,6 @@ sealed class BookDatabaseEntryFormatHandle {
@Throws(IOException::class)
abstract fun copyInBook(file: File)
-
- /**
- * Set the last read location for the book.
- *
- * @param bookmark The location
- *
- * @throws IOException On I/O errors
- */
-
- @Throws(IOException::class)
- abstract fun setLastReadLocation(bookmark: Bookmark.ReaderBookmark?)
-
- /**
- * Set the bookmarks for the book.
- *
- * @param bookmarks The list of bookmarks
- *
- * @throws IOException On I/O errors
- */
-
- @Throws(IOException::class)
- abstract fun setBookmarks(bookmarks: List)
}
/**
@@ -234,27 +242,6 @@ sealed class BookDatabaseEntryFormatHandle {
@Throws(IOException::class)
abstract fun copyInBook(file: File)
-
- /**
- * Set the last read location for the PDF book.
- *
- * @param bookmark The bookmark of the PDF book
- *
- * @throws IOException On I/O errors
- */
- @Throws(IOException::class)
- abstract fun setLastReadLocation(bookmark: Bookmark.PDFBookmark?)
-
- /**
- * Set the bookmarks for the book.
- *
- * @param bookmarks The list of bookmarks
- *
- * @throws IOException On I/O errors
- */
-
- @Throws(IOException::class)
- abstract fun setBookmarks(bookmarks: List)
}
/**
@@ -302,27 +289,5 @@ sealed class BookDatabaseEntryFormatHandle {
@Throws(IOException::class)
abstract fun moveInBook(file: File)
-
- /**
- * Set the last read location for the book.
- *
- * @param bookmark The location
- *
- * @throws IOException On I/O errors
- */
-
- @Throws(IOException::class)
- abstract fun setLastReadLocation(bookmark: Bookmark.AudiobookBookmark?)
-
- /**
- * Set the bookmarks for the book.
- *
- * @param bookmarks The list of bookmarks
- *
- * @throws IOException On I/O errors
- */
-
- @Throws(IOException::class)
- abstract fun setBookmarks(bookmarks: List)
}
}
diff --git a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleAudioBook.kt b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleAudioBook.kt
index 83c64c24b..9a5f48c4b 100644
--- a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleAudioBook.kt
+++ b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleAudioBook.kt
@@ -6,18 +6,18 @@ import net.jcip.annotations.GuardedBy
import one.irradia.mime.api.MIMEType
import org.librarysimplified.audiobook.api.PlayerAudioEngineRequest
import org.librarysimplified.audiobook.api.PlayerAudioEngines
-import org.librarysimplified.audiobook.api.PlayerPositions
import org.librarysimplified.audiobook.api.PlayerResult
import org.librarysimplified.audiobook.api.PlayerUserAgent
import org.librarysimplified.audiobook.manifest.api.PlayerManifest
import org.librarysimplified.audiobook.manifest_parser.api.ManifestParsers
import org.librarysimplified.audiobook.parser.api.ParseResult
-import org.nypl.simplified.books.api.BookDRMKind
import org.nypl.simplified.books.api.BookDRMInformation
+import org.nypl.simplified.books.api.BookDRMKind
import org.nypl.simplified.books.api.BookFormat
-import org.nypl.simplified.books.api.bookmark.Bookmark
-import org.nypl.simplified.books.api.bookmark.BookmarkJSON
+import org.nypl.simplified.books.api.bookmark.BookmarkID
import org.nypl.simplified.books.api.bookmark.BookmarkKind
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
+import org.nypl.simplified.books.api.bookmark.SerializedBookmarks
import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle
import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook
import org.nypl.simplified.files.DirectoryUtilities
@@ -29,7 +29,6 @@ import org.slf4j.LoggerFactory
import java.io.File
import java.io.FileInputStream
import java.io.IOException
-import java.lang.IllegalStateException
import java.net.URI
/**
@@ -238,6 +237,26 @@ internal class DatabaseFormatHandleAudioBook internal constructor(
this.parameters.onUpdated.invoke(newFormat)
}
+ override fun deleteBookmark(bookmarkId: BookmarkID) {
+ val newFormat = synchronized(this.dataLock) {
+ val serialized = this.formatRef.bookmarks.filter { bookmark ->
+ bookmark.bookmarkId != bookmarkId
+ }
+
+ FileUtilities.fileWriteUTF8Atomically(
+ this.fileBookmarks,
+ this.fileBookmarksTmp,
+ JSONSerializerUtilities.serializeToString(
+ serialized.map { x -> x.toJSON(this.parameters.objectMapper) }
+ )
+ )
+ this.formatRef = this.formatRef.copy(bookmarks = serialized)
+ this.formatRef
+ }
+
+ this.parameters.onUpdated.invoke(newFormat)
+ }
+
override fun copyInManifestAndURI(
data: ByteArray,
manifestURI: URI
@@ -301,21 +320,7 @@ internal class DatabaseFormatHandleAudioBook internal constructor(
this.parameters.onUpdated.invoke(newFormat)
}
- override fun setBookmarks(bookmarks: List) {
- val newFormat = synchronized(this.dataLock) {
- FileUtilities.fileWriteUTF8Atomically(
- this.fileBookmarks,
- this.fileBookmarksTmp,
- BookmarkJSON.serializeAudiobookBookmarksToString(this.parameters.objectMapper, bookmarks)
- )
- this.formatRef = this.formatRef.copy(bookmarks = bookmarks)
- this.formatRef
- }
-
- this.parameters.onUpdated.invoke(newFormat)
- }
-
- override fun setLastReadLocation(bookmark: Bookmark.AudiobookBookmark?) {
+ override fun setLastReadLocation(bookmark: SerializedBookmark?) {
val newFormat = synchronized(this.dataLock) {
if (bookmark != null) {
Preconditions.checkArgument(
@@ -326,9 +331,7 @@ internal class DatabaseFormatHandleAudioBook internal constructor(
FileUtilities.fileWriteUTF8Atomically(
this.filePosition,
this.filePositionTmp,
- JSONSerializerUtilities.serializeToString(
- PlayerPositions.serializeToObjectNode(bookmark.location)
- )
+ JSONSerializerUtilities.serializeToString(bookmark.toJSON(this.parameters.objectMapper))
)
} else {
FileUtilities.fileDelete(this.filePosition)
@@ -341,6 +344,29 @@ internal class DatabaseFormatHandleAudioBook internal constructor(
this.parameters.onUpdated.invoke(newFormat)
}
+ override fun addBookmark(
+ bookmark: SerializedBookmark
+ ) {
+ val newFormat = synchronized(this.dataLock) {
+ val newBookmarks = arrayListOf()
+ newBookmarks.addAll(this.formatRef.bookmarks)
+ newBookmarks.removeIf { b -> b.bookmarkId == bookmark.bookmarkId }
+ newBookmarks.add(bookmark)
+
+ FileUtilities.fileWriteUTF8Atomically(
+ this.fileBookmarks,
+ this.fileBookmarksTmp,
+ JSONSerializerUtilities.serializeToString(
+ newBookmarks.map { x -> x.toJSON(this.parameters.objectMapper) }
+ )
+ )
+ this.formatRef = this.formatRef.copy(bookmarks = newBookmarks)
+ this.formatRef
+ }
+
+ this.parameters.onUpdated.invoke(newFormat)
+ }
+
companion object {
private val logger =
@@ -361,8 +387,8 @@ internal class DatabaseFormatHandleAudioBook internal constructor(
manifest = this.loadManifestIfNecessary(fileManifest, fileManifestURI),
contentType = contentType,
drmInformation = drmInfo,
- bookmarks = loadBookmarksIfPresent(objectMapper, fileBookmarks),
- lastReadLocation = loadLastReadLocationIfPresent(objectMapper, filePosition),
+ bookmarks = this.loadBookmarksIfPresent(objectMapper, fileBookmarks),
+ lastReadLocation = this.loadLastReadLocationIfPresent(filePosition),
)
}
@@ -370,9 +396,9 @@ internal class DatabaseFormatHandleAudioBook internal constructor(
private fun loadBookmarksIfPresent(
objectMapper: ObjectMapper,
fileBookmarks: File
- ): List {
+ ): List {
return if (fileBookmarks.isFile) {
- loadBookmarks(
+ this.loadBookmarks(
objectMapper = objectMapper,
fileBookmarks = fileBookmarks
)
@@ -384,41 +410,33 @@ internal class DatabaseFormatHandleAudioBook internal constructor(
private fun loadBookmarks(
objectMapper: ObjectMapper,
fileBookmarks: File
- ): List {
- val tree = objectMapper.readTree(fileBookmarks)
- val array = JSONParserUtilities.checkArray(null, tree)
-
- val bookmarks = arrayListOf()
+ ): List {
+ val tree =
+ objectMapper.readTree(fileBookmarks)
+ val array =
+ JSONParserUtilities.checkArray(null, tree)
+ val bookmarks =
+ arrayListOf()
array.forEach { node ->
try {
- val bookmark = BookmarkJSON.deserializeAudiobookBookmarkFromJSON(
- kind = BookmarkKind.BookmarkExplicit,
- node = node
- )
-
- bookmarks.add(bookmark)
+ bookmarks.add(SerializedBookmarks.parseBookmark(node))
} catch (exception: JSONParseException) {
- this.logger.debug("Failed to parse an audiobook bookmark from the bookmarks file")
+ this.logger.debug("Failed to parse bookmark: ", exception)
}
}
-
return bookmarks
}
@Throws(IOException::class)
private fun loadLastReadLocationIfPresent(
- objectMapper: ObjectMapper,
fileLastRead: File
- ): Bookmark.AudiobookBookmark? {
+ ): SerializedBookmark? {
return if (fileLastRead.isFile) {
try {
- loadLastReadLocation(
- objectMapper = objectMapper,
- fileLastRead = fileLastRead
- )
+ this.loadLastReadLocation(fileLastRead = fileLastRead)
} catch (e: Exception) {
- logger.error("failed to read the last-read location: ", e)
+ this.logger.error("Failed to read the last-read location: ", e)
null
}
} else {
@@ -428,15 +446,9 @@ internal class DatabaseFormatHandleAudioBook internal constructor(
@Throws(IOException::class)
private fun loadLastReadLocation(
- objectMapper: ObjectMapper,
fileLastRead: File
- ): Bookmark.AudiobookBookmark {
- val serialized = FileUtilities.fileReadUTF8(fileLastRead)
- return BookmarkJSON.deserializeAudiobookBookmarkFromString(
- objectMapper = objectMapper,
- kind = BookmarkKind.BookmarkLastReadLocation,
- serialized = serialized
- )
+ ): SerializedBookmark {
+ return SerializedBookmarks.parseBookmarkFromString(FileUtilities.fileReadUTF8(fileLastRead))
}
private fun loadManifestIfNecessary(
diff --git a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleEPUB.kt b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleEPUB.kt
index b4316e78b..7356d8a16 100644
--- a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleEPUB.kt
+++ b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleEPUB.kt
@@ -7,15 +7,17 @@ import one.irradia.mime.api.MIMEType
import org.nypl.simplified.books.api.BookDRMInformation
import org.nypl.simplified.books.api.BookDRMKind
import org.nypl.simplified.books.api.BookFormat
-import org.nypl.simplified.books.api.bookmark.Bookmark
-import org.nypl.simplified.books.api.bookmark.BookmarkJSON
+import org.nypl.simplified.books.api.bookmark.BookmarkID
import org.nypl.simplified.books.api.bookmark.BookmarkKind
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
+import org.nypl.simplified.books.api.bookmark.SerializedBookmarks
import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle
import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB
import org.nypl.simplified.files.DirectoryUtilities
import org.nypl.simplified.files.FileUtilities
import org.nypl.simplified.json.core.JSONParseException
import org.nypl.simplified.json.core.JSONParserUtilities
+import org.nypl.simplified.json.core.JSONSerializerUtilities
import org.slf4j.LoggerFactory
import java.io.File
import java.io.IOException
@@ -136,6 +138,26 @@ internal class DatabaseFormatHandleEPUB internal constructor(
this.parameters.onUpdated.invoke(newFormat)
}
+ override fun deleteBookmark(bookmarkId: BookmarkID) {
+ val newFormat = synchronized(this.dataLock) {
+ val serialized = this.formatRef.bookmarks.filter { bookmark ->
+ bookmark.bookmarkId != bookmarkId
+ }
+
+ FileUtilities.fileWriteUTF8Atomically(
+ this.fileBookmarks,
+ this.fileBookmarksTmp,
+ JSONSerializerUtilities.serializeToString(
+ serialized.map { x -> x.toJSON(this.parameters.objectMapper) }
+ )
+ )
+ this.formatRef = this.formatRef.copy(bookmarks = serialized)
+ this.formatRef
+ }
+
+ this.parameters.onUpdated.invoke(newFormat)
+ }
+
override fun copyInBook(file: File) {
val newFormat = synchronized(this.dataLock) {
if (file.isDirectory) {
@@ -151,7 +173,7 @@ internal class DatabaseFormatHandleEPUB internal constructor(
this.parameters.onUpdated.invoke(newFormat)
}
- override fun setLastReadLocation(bookmark: Bookmark.ReaderBookmark?) {
+ override fun setLastReadLocation(bookmark: SerializedBookmark?) {
val newFormat = synchronized(this.dataLock) {
if (bookmark != null) {
Preconditions.checkArgument(
@@ -162,7 +184,7 @@ internal class DatabaseFormatHandleEPUB internal constructor(
FileUtilities.fileWriteUTF8Atomically(
this.fileLastRead,
this.fileLastReadTmp,
- BookmarkJSON.serializeReaderBookmarkToString(this.parameters.objectMapper, bookmark)
+ JSONSerializerUtilities.serializeToString(bookmark.toJSON(this.parameters.objectMapper))
)
} else {
FileUtilities.fileDelete(this.fileLastRead)
@@ -175,14 +197,23 @@ internal class DatabaseFormatHandleEPUB internal constructor(
this.parameters.onUpdated.invoke(newFormat)
}
- override fun setBookmarks(bookmarks: List) {
+ override fun addBookmark(
+ bookmark: SerializedBookmark
+ ) {
val newFormat = synchronized(this.dataLock) {
+ val newBookmarks = arrayListOf()
+ newBookmarks.addAll(this.formatRef.bookmarks)
+ newBookmarks.removeIf { b -> b.bookmarkId == bookmark.bookmarkId }
+ newBookmarks.add(bookmark)
+
FileUtilities.fileWriteUTF8Atomically(
this.fileBookmarks,
this.fileBookmarksTmp,
- BookmarkJSON.serializeReaderBookmarksToString(this.parameters.objectMapper, bookmarks)
+ JSONSerializerUtilities.serializeToString(
+ newBookmarks.map { x -> x.toJSON(this.parameters.objectMapper) }
+ )
)
- this.formatRef = this.formatRef.copy(bookmarks = bookmarks)
+ this.formatRef = this.formatRef.copy(bookmarks = newBookmarks)
this.formatRef
}
@@ -204,9 +235,9 @@ internal class DatabaseFormatHandleEPUB internal constructor(
drmInfo: BookDRMInformation
): BookFormat.BookFormatEPUB {
return BookFormat.BookFormatEPUB(
- bookmarks = loadBookmarksIfPresent(objectMapper, fileBookmarks),
+ bookmarks = this.loadBookmarksIfPresent(objectMapper, fileBookmarks),
file = if (fileBook.exists()) fileBook else null,
- lastReadLocation = loadLastReadLocationIfPresent(objectMapper, fileLastRead),
+ lastReadLocation = this.loadLastReadLocationIfPresent(fileLastRead),
contentType = contentType,
drmInformation = drmInfo
)
@@ -216,9 +247,9 @@ internal class DatabaseFormatHandleEPUB internal constructor(
private fun loadBookmarksIfPresent(
objectMapper: ObjectMapper,
fileBookmarks: File
- ): List {
+ ): List {
return if (fileBookmarks.isFile) {
- loadBookmarks(
+ this.loadBookmarks(
objectMapper = objectMapper,
fileBookmarks = fileBookmarks
)
@@ -230,40 +261,33 @@ internal class DatabaseFormatHandleEPUB internal constructor(
private fun loadBookmarks(
objectMapper: ObjectMapper,
fileBookmarks: File
- ): List {
- val tree = objectMapper.readTree(fileBookmarks)
- val array = JSONParserUtilities.checkArray(null, tree)
-
- val bookmarks = arrayListOf()
+ ): List {
+ val tree =
+ objectMapper.readTree(fileBookmarks)
+ val array =
+ JSONParserUtilities.checkArray(null, tree)
+ val bookmarks =
+ arrayListOf()
array.forEach { node ->
try {
- val bookmark = BookmarkJSON.deserializeReaderBookmarkFromJSON(
- kind = BookmarkKind.BookmarkExplicit,
- node = node
- )
- bookmarks.add(bookmark)
+ bookmarks.add(SerializedBookmarks.parseBookmark(node))
} catch (exception: JSONParseException) {
- this.logger.debug("There was an error parsing the reader bookmark from bookmarks file")
+ this.logger.debug("Failed to parse bookmark: ", exception)
}
}
-
return bookmarks
}
@Throws(IOException::class)
private fun loadLastReadLocationIfPresent(
- objectMapper: ObjectMapper,
fileLastRead: File
- ): Bookmark.ReaderBookmark? {
+ ): SerializedBookmark? {
return if (fileLastRead.isFile) {
try {
- loadLastReadLocation(
- objectMapper = objectMapper,
- fileLastRead = fileLastRead
- )
+ this.loadLastReadLocation(fileLastRead = fileLastRead)
} catch (e: Exception) {
- logger.error("failed to read the last-read location: ", e)
+ this.logger.error("Failed to read the last-read location: ", e)
null
}
} else {
@@ -273,15 +297,9 @@ internal class DatabaseFormatHandleEPUB internal constructor(
@Throws(IOException::class)
private fun loadLastReadLocation(
- objectMapper: ObjectMapper,
fileLastRead: File
- ): Bookmark.ReaderBookmark {
- val serialized = FileUtilities.fileReadUTF8(fileLastRead)
- return BookmarkJSON.deserializeReaderBookmarkFromString(
- objectMapper = objectMapper,
- kind = BookmarkKind.BookmarkLastReadLocation,
- serialized = serialized
- )
+ ): SerializedBookmark {
+ return SerializedBookmarks.parseBookmarkFromString(FileUtilities.fileReadUTF8(fileLastRead))
}
}
}
diff --git a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandlePDF.kt b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandlePDF.kt
index c42226ea6..8b40ba3fd 100644
--- a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandlePDF.kt
+++ b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandlePDF.kt
@@ -3,23 +3,21 @@ package org.nypl.simplified.books.book_database
import com.fasterxml.jackson.databind.ObjectMapper
import net.jcip.annotations.GuardedBy
import one.irradia.mime.api.MIMEType
-import org.joda.time.DateTime
import org.nypl.simplified.books.api.BookDRMInformation
import org.nypl.simplified.books.api.BookDRMKind
import org.nypl.simplified.books.api.BookFormat
-import org.nypl.simplified.books.api.bookmark.Bookmark
-import org.nypl.simplified.books.api.bookmark.BookmarkJSON
-import org.nypl.simplified.books.api.bookmark.BookmarkKind
+import org.nypl.simplified.books.api.bookmark.BookmarkID
+import org.nypl.simplified.books.api.bookmark.SerializedBookmark
+import org.nypl.simplified.books.api.bookmark.SerializedBookmarks
import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle
import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandlePDF
import org.nypl.simplified.files.FileUtilities
import org.nypl.simplified.json.core.JSONParseException
import org.nypl.simplified.json.core.JSONParserUtilities
+import org.nypl.simplified.json.core.JSONSerializerUtilities
import org.slf4j.LoggerFactory
import java.io.File
import java.io.IOException
-import java.lang.IllegalStateException
-import java.lang.NumberFormatException
/**
* Operations on PDF formats in database entries.
@@ -129,6 +127,26 @@ internal class DatabaseFormatHandlePDF internal constructor(
this.parameters.onUpdated.invoke(newFormat)
}
+ override fun deleteBookmark(bookmarkId: BookmarkID) {
+ val newFormat = synchronized(this.dataLock) {
+ val serialized = this.formatRef.bookmarks.filter {
+ bookmark -> bookmark.bookmarkId != bookmarkId
+ }
+
+ FileUtilities.fileWriteUTF8Atomically(
+ this.fileBookmarks,
+ this.fileBookmarksTmp,
+ JSONSerializerUtilities.serializeToString(
+ serialized.map { x -> x.toJSON(this.parameters.objectMapper) }
+ )
+ )
+ this.formatRef = this.formatRef.copy(bookmarks = serialized)
+ this.formatRef
+ }
+
+ this.parameters.onUpdated.invoke(newFormat)
+ }
+
override fun copyInBook(file: File) {
val newFormat = synchronized(this.dataLock) {
FileUtilities.fileCopy(file, this.fileBook)
@@ -139,13 +157,13 @@ internal class DatabaseFormatHandlePDF internal constructor(
this.parameters.onUpdated.invoke(newFormat)
}
- override fun setLastReadLocation(bookmark: Bookmark.PDFBookmark?) {
+ override fun setLastReadLocation(bookmark: SerializedBookmark?) {
val newFormat = synchronized(this.dataLock) {
if (bookmark != null) {
FileUtilities.fileWriteUTF8Atomically(
this.fileLastRead,
this.fileLastReadTmp,
- BookmarkJSON.serializePdfBookmarkToString(this.parameters.objectMapper, bookmark)
+ JSONSerializerUtilities.serializeToString(bookmark.toJSON(this.parameters.objectMapper))
)
} else {
FileUtilities.fileDelete(this.fileLastRead)
@@ -158,14 +176,23 @@ internal class DatabaseFormatHandlePDF internal constructor(
this.parameters.onUpdated.invoke(newFormat)
}
- override fun setBookmarks(bookmarks: List) {
+ override fun addBookmark(
+ bookmark: SerializedBookmark
+ ) {
val newFormat = synchronized(this.dataLock) {
+ val newBookmarks = arrayListOf()
+ newBookmarks.addAll(this.formatRef.bookmarks)
+ newBookmarks.removeIf { b -> b.bookmarkId == bookmark.bookmarkId }
+ newBookmarks.add(bookmark)
+
FileUtilities.fileWriteUTF8Atomically(
this.fileBookmarks,
this.fileBookmarksTmp,
- BookmarkJSON.serializePdfBookmarksToString(this.parameters.objectMapper, bookmarks)
+ JSONSerializerUtilities.serializeToString(
+ newBookmarks.map { x -> x.toJSON(this.parameters.objectMapper) }
+ )
)
- this.formatRef = this.formatRef.copy(bookmarks = bookmarks)
+ this.formatRef = this.formatRef.copy(bookmarks = newBookmarks)
this.formatRef
}
@@ -189,7 +216,7 @@ internal class DatabaseFormatHandlePDF internal constructor(
return BookFormat.BookFormatPDF(
bookmarks = loadBookmarksIfPresent(objectMapper, fileBookmarks),
file = if (fileBook.isFile) fileBook else null,
- lastReadLocation = loadLastReadLocationIfPresent(objectMapper, fileLastRead),
+ lastReadLocation = loadLastReadLocationIfPresent(fileLastRead),
contentType = contentType,
drmInformation = drmInfo
)
@@ -199,7 +226,7 @@ internal class DatabaseFormatHandlePDF internal constructor(
private fun loadBookmarksIfPresent(
objectMapper: ObjectMapper,
fileBookmarks: File
- ): List {
+ ): List {
return if (fileBookmarks.isFile) {
loadBookmarks(
objectMapper = objectMapper,
@@ -213,59 +240,42 @@ internal class DatabaseFormatHandlePDF internal constructor(
private fun loadBookmarks(
objectMapper: ObjectMapper,
fileBookmarks: File
- ): List {
- val tree = objectMapper.readTree(fileBookmarks)
- val array = JSONParserUtilities.checkArray(null, tree)
-
- val bookmarks = arrayListOf()
+ ): List {
+ val tree =
+ objectMapper.readTree(fileBookmarks)
+ val array =
+ JSONParserUtilities.checkArray(null, tree)
+ val bookmarks =
+ arrayListOf()
array.forEach { node ->
try {
- val bookmark = BookmarkJSON.deserializePdfBookmarkFromJSON(
- kind = BookmarkKind.BookmarkExplicit,
- node = node
- )
- bookmarks.add(bookmark)
- } catch (exception: JSONParseException) {
- this.logger.debug("There was an error parsing the pdf bookmark from bookmarks file")
+ bookmarks.add(SerializedBookmarks.parseBookmark(node))
+ } catch (e: JSONParseException) {
+ this.logger.debug("Failed to parse bookmark: ", e)
}
}
-
return bookmarks
}
@Throws(IOException::class)
private fun loadLastReadLocation(
- objectMapper: ObjectMapper,
fileLastRead: File
- ): Bookmark.PDFBookmark {
+ ): SerializedBookmark? {
val serialized = FileUtilities.fileReadUTF8(fileLastRead)
return try {
- Bookmark.PDFBookmark.create(
- opdsId = "",
- kind = BookmarkKind.BookmarkLastReadLocation,
- time = DateTime.now(),
- pageNumber = serialized.toInt(),
- deviceID = "",
- uri = null
- )
+ SerializedBookmarks.parseBookmarkFromString(serialized)
} catch (exception: NumberFormatException) {
- this.logger.debug("The stored bookmark is not from the older version")
- BookmarkJSON.deserializePdfBookmarkFromString(
- objectMapper = objectMapper,
- kind = BookmarkKind.BookmarkLastReadLocation,
- serialized = serialized
- )
+ null
}
}
@Throws(IOException::class)
private fun loadLastReadLocationIfPresent(
- objectMapper: ObjectMapper,
fileLastRead: File
- ): Bookmark.PDFBookmark? {
+ ): SerializedBookmark? {
return if (fileLastRead.isFile) {
- loadLastReadLocation(objectMapper, fileLastRead)
+ loadLastReadLocation(fileLastRead)
} else {
null
}
diff --git a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/NullDownloadProvider.kt b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/NullDownloadProvider.kt
index 392f54fc1..85b3568cd 100644
--- a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/NullDownloadProvider.kt
+++ b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/NullDownloadProvider.kt
@@ -1,16 +1,17 @@
package org.nypl.simplified.books.book_database
-import com.google.common.util.concurrent.Futures
-import com.google.common.util.concurrent.ListenableFuture
import org.librarysimplified.audiobook.api.PlayerDownloadProviderType
import org.librarysimplified.audiobook.api.PlayerDownloadRequest
+import java.util.concurrent.CompletableFuture
/**
* A download provider that does nothing.
*/
internal class NullDownloadProvider : PlayerDownloadProviderType {
- override fun download(request: PlayerDownloadRequest): ListenableFuture {
- return Futures.immediateFailedFuture(UnsupportedOperationException())
+ override fun download(request: PlayerDownloadRequest): CompletableFuture {
+ val future = CompletableFuture()
+ future.completeExceptionally(UnsupportedOperationException())
+ return future
}
}
diff --git a/simplified-books-registry-api/src/main/java/org/nypl/simplified/books/book_registry/BookRegistryReadableType.kt b/simplified-books-registry-api/src/main/java/org/nypl/simplified/books/book_registry/BookRegistryReadableType.kt
index 87bc10b05..5621e4fb0 100644
--- a/simplified-books-registry-api/src/main/java/org/nypl/simplified/books/book_registry/BookRegistryReadableType.kt
+++ b/simplified-books-registry-api/src/main/java/org/nypl/simplified/books/book_registry/BookRegistryReadableType.kt
@@ -5,10 +5,7 @@ import com.io7m.jfunctional.OptionType
import com.io7m.jfunctional.OptionVisitorType
import com.io7m.jfunctional.Some
import io.reactivex.Observable
-
import org.nypl.simplified.books.api.BookID
-
-import java.util.NoSuchElementException
import java.util.SortedMap
/**
diff --git a/simplified-books-time-tracking/src/main/java/org/nypl/simplified/books/time/tracking/TimeTrackingService.kt b/simplified-books-time-tracking/src/main/java/org/nypl/simplified/books/time/tracking/TimeTrackingService.kt
index ee4ca93cf..632ccce16 100644
--- a/simplified-books-time-tracking/src/main/java/org/nypl/simplified/books/time/tracking/TimeTrackingService.kt
+++ b/simplified-books-time-tracking/src/main/java/org/nypl/simplified/books/time/tracking/TimeTrackingService.kt
@@ -14,8 +14,8 @@ import org.nypl.simplified.profiles.controller.api.ProfilesControllerType
import org.slf4j.LoggerFactory
import java.io.File
import java.net.URI
-import java.util.concurrent.TimeUnit
import java.util.UUID
+import java.util.concurrent.TimeUnit
import kotlin.math.min
class TimeTrackingService(
@@ -136,8 +136,8 @@ class TimeTrackingService(
}
when (playerEvent) {
- is PlayerEvent.PlayerEventWithSpineElement.PlayerEventPlaybackProgressUpdate,
- is PlayerEvent.PlayerEventWithSpineElement.PlayerEventPlaybackStarted -> {
+ is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackProgressUpdate,
+ is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackStarted -> {
isPlaying = true
if (audiobookPlayingDisposable == null) {
createTimeTrackingEntry()
@@ -145,20 +145,31 @@ class TimeTrackingService(
}
}
- is PlayerEvent.PlayerEventWithSpineElement.PlayerEventPlaybackBuffering,
- is PlayerEvent.PlayerEventWithSpineElement.PlayerEventPlaybackWaitingForAction,
- is PlayerEvent.PlayerEventWithSpineElement.PlayerEventChapterWaiting,
- is PlayerEvent.PlayerEventWithSpineElement.PlayerEventPlaybackPaused,
- is PlayerEvent.PlayerEventWithSpineElement.PlayerEventPlaybackStopped,
- is PlayerEvent.PlayerEventWithSpineElement.PlayerEventChapterCompleted -> {
+ is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackBuffering,
+ is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackWaitingForAction,
+ is PlayerEvent.PlayerEventWithPosition.PlayerEventChapterWaiting,
+ is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackPaused,
+ is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackStopped,
+ is PlayerEvent.PlayerEventWithPosition.PlayerEventChapterCompleted -> {
isPlaying = false
}
- is PlayerEvent.PlayerEventWithSpineElement.PlayerEventCreateBookmark,
+ is PlayerEvent.PlayerEventWithPosition.PlayerEventCreateBookmark,
is PlayerEvent.PlayerEventPlaybackRateChanged,
is PlayerEvent.PlayerEventError,
PlayerEvent.PlayerEventManifestUpdated -> {
// do nothing
}
+
+ is PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityChapterSelected,
+ is PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityErrorOccurred,
+ is PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityIsBuffering,
+ is PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityIsWaitingForChapter,
+ is PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityPlaybackRateChanged,
+ is PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilitySleepTimerSettingChanged,
+ is PlayerEvent.PlayerEventDeleteBookmark,
+ is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackPreparing -> {
+ // do nothing
+ }
}
}
diff --git a/simplified-feeds-api/src/main/java/org/nypl/simplified/feeds/api/Feed.kt b/simplified-feeds-api/src/main/java/org/nypl/simplified/feeds/api/Feed.kt
index 422113139..d2e7768c6 100644
--- a/simplified-feeds-api/src/main/java/org/nypl/simplified/feeds/api/Feed.kt
+++ b/simplified-feeds-api/src/main/java/org/nypl/simplified/feeds/api/Feed.kt
@@ -18,9 +18,7 @@ import org.nypl.simplified.opds.core.OPDSAcquisitionFeed
import org.nypl.simplified.opds.core.OPDSAcquisitionFeedEntry
import org.nypl.simplified.opds.core.OPDSOpenSearch1_1
import java.net.URI
-import java.util.ArrayList
import java.util.Collections
-import java.util.HashMap
/**
* The type of mutable feeds.
diff --git a/simplified-files/src/main/java/org/nypl/simplified/files/FileLocking.java b/simplified-files/src/main/java/org/nypl/simplified/files/FileLocking.java
index 7bc6f6814..295958adc 100644
--- a/simplified-files/src/main/java/org/nypl/simplified/files/FileLocking.java
+++ b/simplified-files/src/main/java/org/nypl/simplified/files/FileLocking.java
@@ -4,6 +4,7 @@
import com.io7m.jfunctional.Unit;
import com.io7m.jnull.NullCheck;
import com.io7m.junreachable.UnreachableCodeException;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.java b/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.java
deleted file mode 100644
index 8ca58893b..000000000
--- a/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.java
+++ /dev/null
@@ -1,938 +0,0 @@
-package org.nypl.simplified.json.core;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.io7m.jfunctional.None;
-import com.io7m.jfunctional.Option;
-import com.io7m.jfunctional.OptionType;
-import com.io7m.jfunctional.OptionVisitorType;
-import com.io7m.jfunctional.Some;
-import com.io7m.jnull.NullCheck;
-import com.io7m.jnull.Nullable;
-import com.io7m.junreachable.UnreachableCodeException;
-
-import org.joda.time.DateTime;
-import org.joda.time.format.ISODateTimeFormat;
-
-import java.math.BigInteger;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Objects;
-
-/**
- * Utility functions for deserializing elements from JSON.
- *
- *
The functions take a strict approach: Types are checked upon key retrieval
- * and exceptions are raised if the type is not exactly as expected.
- */
-
-public final class JSONParserUtilities {
- private JSONParserUtilities() {
- throw new UnreachableCodeException();
- }
-
- /**
- * Check that {@code n} is an object.
- *
- * @param key An optional advisory key to be used in error messages
- * @param n A node
- * @return {@code n} as an {@link ObjectNode}
- * @throws JSONParseException On type errors
- */
-
- public static ObjectNode checkObject(
- final @Nullable String key,
- final JsonNode n)
- throws JSONParseException {
-
- NullCheck.notNull(n);
-
- switch (n.getNodeType()) {
- case ARRAY:
- case BINARY:
- case BOOLEAN:
- case MISSING:
- case NULL:
- case NUMBER:
- case POJO:
- case STRING: {
- final StringBuilder sb = new StringBuilder(128);
- if (key != null) {
- sb.append("Expected: A key '");
- sb.append(key);
- sb.append("' with a value of type Object\n");
- sb.append("Got: A value of type ");
- sb.append(n.getNodeType());
- sb.append("\n");
- } else {
- sb.append("Expected: A value of type Object\n");
- sb.append("Got: A value of type ");
- sb.append(n.getNodeType());
- sb.append("\n");
- }
-
- final String m = NullCheck.notNull(sb.toString());
- throw new JSONParseException(m);
- }
- case OBJECT: {
- return (ObjectNode) n;
- }
- }
-
- throw new UnreachableCodeException();
- }
-
- /**
- * Check that {@code n} is an array.
- *
- * @param key An optional advisory key to be used in error messages
- * @param n A node
- * @return {@code n} as an {@link ObjectNode}
- * @throws JSONParseException On type errors
- */
-
- public static ArrayNode checkArray(
- final @Nullable String key,
- final JsonNode n)
- throws JSONParseException {
-
- NullCheck.notNull(n);
-
- switch (n.getNodeType()) {
- case ARRAY: {
- return (ArrayNode) n;
- }
- case BINARY:
- case BOOLEAN:
- case MISSING:
- case NULL:
- case NUMBER:
- case POJO:
- case OBJECT:
- case STRING: {
- final StringBuilder sb = new StringBuilder(128);
- if (key != null) {
- sb.append("Expected: A key '");
- sb.append(key);
- sb.append("' with a value of type Object\n");
- sb.append("Got: A value of type ");
- sb.append(n.getNodeType());
- sb.append("\n");
- } else {
- sb.append("Expected: A value of type Object\n");
- sb.append("Got: A value of type ");
- sb.append(n.getNodeType());
- sb.append("\n");
- }
-
- final String m = NullCheck.notNull(sb.toString());
- throw new JSONParseException(m);
- }
- }
-
- throw new UnreachableCodeException();
- }
-
- /**
- * Check that {@code n} is a string.
- *
- * @param n A node
- * @return {@code n} as a String
- * @throws JSONParseException On type errors
- */
-
- public static String checkString(
- final JsonNode n)
- throws JSONParseException {
-
- NullCheck.notNull(n);
-
- switch (n.getNodeType()) {
- case STRING: {
- return n.asText();
- }
- case ARRAY:
- case BINARY:
- case BOOLEAN:
- case MISSING:
- case NULL:
- case NUMBER:
- case POJO:
- case OBJECT: {
- final StringBuilder sb = new StringBuilder(128);
- sb.append("Expected: A value of type String\n");
- sb.append("Got: A value of type ");
- sb.append(n.getNodeType());
- sb.append("\n");
- throw new JSONParseException(NullCheck.notNull(sb.toString()));
- }
- }
-
- throw new UnreachableCodeException();
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param s A node
- * @return An array from key {@code key}
- * @throws JSONParseException On type errors
- */
-
- public static ArrayNode getArray(
- final ObjectNode s,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(s);
- NullCheck.notNull(key);
-
- final JsonNode n = JSONParserUtilities.getNode(s, key);
- switch (n.getNodeType()) {
- case ARRAY: {
- return (ArrayNode) n;
- }
- case BINARY:
- case BOOLEAN:
- case MISSING:
- case NULL:
- case NUMBER:
- case POJO:
- case STRING:
- case OBJECT: {
- final StringBuilder sb = new StringBuilder(128);
- sb.append("Expected: A key '");
- sb.append(key);
- sb.append("' with a value of type Array\n");
- sb.append("Got: A value of type ");
- sb.append(n.getNodeType());
- sb.append("\n");
- final String m = NullCheck.notNull(sb.toString());
- throw new JSONParseException(m);
- }
- }
-
- throw new UnreachableCodeException();
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param s A node
- * @return An array from key {@code key}, or null if the key is not present
- * @throws JSONParseException On type errors
- */
-
- public static ArrayNode getArrayOrNull(
- final ObjectNode s,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(s);
- NullCheck.notNull(key);
-
- if (s.has(key)) {
- return getArray(s, key);
- }
- return null;
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param o A node
- * @return A boolean value from key {@code key}
- * @throws JSONParseException On type errors
- */
-
- public static boolean getBoolean(
- final ObjectNode o,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(o);
- NullCheck.notNull(key);
-
- final JsonNode v = JSONParserUtilities.getNode(o, key);
- switch (v.getNodeType()) {
- case ARRAY:
- case BINARY:
- case MISSING:
- case NULL:
- case OBJECT:
- case POJO:
- case STRING:
- case NUMBER: {
- final StringBuilder sb = new StringBuilder(128);
- sb.append("Expected: A key '");
- sb.append(key);
- sb.append("' with a value of type Boolean\n");
- sb.append("Got: A value of type ");
- sb.append(v.getNodeType());
- sb.append("\n");
- final String m = NullCheck.notNull(sb.toString());
- throw new JSONParseException(m);
- }
- case BOOLEAN: {
- return v.asBoolean();
- }
- }
-
- throw new UnreachableCodeException();
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return An integer value from key {@code key}
- * @throws JSONParseException On type errors
- */
-
- public static int getInteger(
- final ObjectNode n,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- final JsonNode v = JSONParserUtilities.getNode(n, key);
- switch (v.getNodeType()) {
- case ARRAY:
- case BINARY:
- case BOOLEAN:
- case MISSING:
- case NULL:
- case OBJECT:
- case POJO:
- case STRING: {
- final StringBuilder sb = new StringBuilder(128);
- sb.append("Expected: A key '");
- sb.append(key);
- sb.append("' with a value of type Integer\n");
- sb.append("Got: A value of type ");
- sb.append(v.getNodeType());
- sb.append("\n");
- final String m = NullCheck.notNull(sb.toString());
- throw new JSONParseException(m);
- }
- case NUMBER: {
- return v.asInt();
- }
- }
-
- throw new UnreachableCodeException();
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return A double value from key {@code key}
- * @throws JSONParseException On type errors
- */
-
- public static double getDouble(
- final ObjectNode n,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- final JsonNode v = JSONParserUtilities.getNode(n, key);
- switch (v.getNodeType()) {
- case ARRAY:
- case BINARY:
- case BOOLEAN:
- case MISSING:
- case NULL:
- case OBJECT:
- case POJO:
- case STRING: {
- final StringBuilder sb = new StringBuilder(128);
- sb.append("Expected: A key '");
- sb.append(key);
- sb.append("' with a value of type Double\n");
- sb.append("Got: A value of type ");
- sb.append(v.getNodeType());
- sb.append("\n");
- final String m = NullCheck.notNull(sb.toString());
- throw new JSONParseException(m);
- }
- case NUMBER: {
- return v.asDouble();
- }
- }
-
- throw new UnreachableCodeException();
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param s A node
- * @return An arbitrary json node from key {@code key}
- * @throws JSONParseException On type errors
- */
-
- public static JsonNode getNode(
- final ObjectNode s,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(s);
- NullCheck.notNull(key);
-
- if (s.has(key)) {
- return NullCheck.notNull(s.get(key));
- }
-
- final StringBuilder sb = new StringBuilder(128);
- sb.append("Expected: A key '");
- sb.append(key);
- sb.append("'\n");
- sb.append("Got: nothing\n");
- final String m = NullCheck.notNull(sb.toString());
- throw new JSONParseException(m);
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param s A node
- * @return An object value from key {@code key}
- * @throws JSONParseException On type errors
- */
-
- public static ObjectNode getObject(
- final ObjectNode s,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(s);
- NullCheck.notNull(key);
-
- final JsonNode n = JSONParserUtilities.getNode(s, key);
- return JSONParserUtilities.checkObject(key, n);
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param s A node
- * @return An object value from key {@code key}, if the key exists
- * @throws JSONParseException On type errors
- */
-
- public static OptionType getObjectOptional(
- final ObjectNode s,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(s);
- NullCheck.notNull(key);
-
- if (s.has(key)) {
- return Option.some(JSONParserUtilities.getObject(s, key));
- }
- return Option.none();
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param s A node
- * @return An object value from key {@code key}, if the key exists
- * @throws JSONParseException On type errors
- */
-
- public static ObjectNode getObjectOrNull(
- final ObjectNode s,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(s);
- NullCheck.notNull(key);
-
- if (s.has(key)) {
- return JSONParserUtilities.getObject(s, key);
- }
- return null;
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param s A node
- * @return A string value from key {@code key}
- * @throws JSONParseException On type errors
- */
-
- public static String getString(
- final ObjectNode s,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(s);
- NullCheck.notNull(key);
-
- final JsonNode v = JSONParserUtilities.getNode(s, key);
- switch (v.getNodeType()) {
- case ARRAY:
- case BINARY:
- case BOOLEAN:
- case MISSING:
- case NULL:
- case NUMBER:
- case OBJECT:
- case POJO: {
- final StringBuilder sb = new StringBuilder(128);
- sb.append("Expected: A key '");
- sb.append(key);
- sb.append("' with a value of type String\n");
- sb.append("Got: A value of type ");
- sb.append(v.getNodeType());
- sb.append("\n");
- final String m = NullCheck.notNull(sb.toString());
- throw new JSONParseException(m);
- }
- case STRING: {
- return NullCheck.notNull(v.asText());
- }
- }
-
- throw new UnreachableCodeException();
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return An integer value from key {@code key}, if the key exists
- * @throws JSONParseException On type errors
- */
-
- public static OptionType getIntegerOptional(
- final ObjectNode n,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- if (n.has(key)) {
- return Option.some(JSONParserUtilities.getInteger(n, key));
- }
- return Option.none();
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return An integer value from key {@code key}, if the key exists
- * @throws JSONParseException On type errors
- */
-
- public static Integer getIntegerOrNull(
- final ObjectNode n,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- if (n.has(key)) {
- return JSONParserUtilities.getInteger(n, key);
- }
- return null;
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return A string value from key {@code key}, if the key exists, or {@code default_value} otherwise.
- * @throws JSONParseException On type errors
- */
-
- public static int getIntegerDefault(
- final ObjectNode n,
- final String key,
- final int default_value)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- return getIntegerOptional(n, key).accept(
- new OptionVisitorType() {
- @Override
- public Integer none(final None none) {
- return default_value;
- }
-
- @Override
- public Integer some(final Some some) {
- return some.get();
- }
- }).intValue();
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return An double value from key {@code key}, if the key exists
- * @throws JSONParseException On type errors
- */
-
- public static OptionType getDoubleOptional(
- final ObjectNode n,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- if (n.has(key)) {
- return Option.some(JSONParserUtilities.getDouble(n, key));
- }
- return Option.none();
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return An double value from key {@code key}, if the key exists, or {@code default_value} otherwise.
- * @throws JSONParseException On type errors
- */
-
- public static double getDoubleDefault(
- final ObjectNode n,
- final String key,
- final double default_value)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- return getDoubleOptional(n, key).accept(
- new OptionVisitorType() {
- @Override
- public Double none(final None none) {
- return default_value;
- }
-
- @Override
- public Double some(final Some some) {
- return some.get();
- }
- });
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return A string value from key {@code key}, if the key exists
- * @throws JSONParseException On type errors
- */
-
- public static OptionType getStringOptional(
- final ObjectNode n,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- if (n.has(key)) {
- if (n.get(key).isNull()) {
- return Option.none();
- }
- return Option.some(JSONParserUtilities.getString(n, key));
- }
- return Option.none();
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return A string value from key {@code key}, if the key exists
- * @throws JSONParseException On type errors
- */
-
- public static String getStringOrNull(
- final ObjectNode n,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- if (n.has(key)) {
- if (n.get(key).isNull()) {
- return null;
- }
- return JSONParserUtilities.getString(n, key);
- }
- return null;
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return A string value from key {@code key}, if the key exists, or {@code default_value} otherwise.
- * @throws JSONParseException On type errors
- */
-
- public static String getStringDefault(
- final ObjectNode n,
- final String key,
- final String default_value)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- return getStringOptional(n, key).accept(
- new OptionVisitorType() {
- @Override
- public String none(final None none) {
- return default_value;
- }
-
- @Override
- public String some(final Some some) {
- return some.get();
- }
- });
- }
-
- /**
- * @param s A node
- * @param key A key assumed to be holding a value
- * @return A timestamp value from key {@code key}
- * @throws JSONParseException On type errors
- */
-
- public static DateTime getTimestamp(
- final ObjectNode s,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(s);
- NullCheck.notNull(key);
-
- try {
- return ISODateTimeFormat.dateTimeParser()
- .withZoneUTC()
- .parseDateTime(JSONParserUtilities.getString(s, key));
- } catch (final IllegalArgumentException e) {
- throw new JSONParseException(
- String.format("Could not parse RFC3999 date for key '%s'", key), e);
- }
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return A timestamp value from key {@code key}, if the key exists
- * @throws JSONParseException On type errors
- */
-
- public static OptionType getTimestampOptional(
- final ObjectNode n,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- if (n.has(key)) {
- return Option.some(JSONParserUtilities.getTimestamp(n, key));
- }
- return Option.none();
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return A URI value from key {@code key}, if the key exists
- * @throws JSONParseException On type errors
- */
-
- public static OptionType getURIOptional(
- final ObjectNode n,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- return JSONParserUtilities.getStringOptional(n, key).mapPartial(
- x -> {
- try {
- return new URI(x);
- } catch (final URISyntaxException e) {
- throw new JSONParseException(e);
- }
- });
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return A URI value from key {@code key}, if the key exists
- * @throws JSONParseException On type errors
- */
-
- public static URI getURIOrNull(
- final ObjectNode n,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- OptionType opt = getURIOptional(n, key);
- if (opt.isSome()) {
- return ((Some) opt).get();
- } else {
- return null;
- }
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return A URI value from key {@code key}
- * @throws JSONParseException On type errors
- */
-
- public static URI getURI(
- final ObjectNode n,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- try {
- return new URI(JSONParserUtilities.getString(n, key).trim());
- } catch (final URISyntaxException e) {
- throw new JSONParseException(e);
- }
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return A URI value from key {@code key}
- * @throws JSONParseException On type errors
- */
-
- public static URI getURIDefault(
- final ObjectNode n,
- final String key,
- final URI default_value)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
- Objects.requireNonNull(default_value, "Default");
-
- return getURIOptional(n, key).accept(new OptionVisitorType() {
- @Override
- public URI none(None n) {
- return default_value;
- }
-
- @Override
- public URI some(Some s) {
- return s.get();
- }
- });
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @param v A default value
- * @return A boolean from key {@code key}, or {@code v} if the key does not
- * exist
- * @throws JSONParseException On type errors
- */
-
- public static boolean getBooleanDefault(
- final ObjectNode n,
- final String key,
- final boolean v)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- if (n.has(key)) {
- return JSONParserUtilities.getBoolean(n, key);
- }
- return v;
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return A big integer value from key {@code key}, if the key exists
- * @throws JSONParseException On type errors
- */
-
- public static OptionType getBigIntegerOptional(
- final ObjectNode n,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- if (n.has(key)) {
- return Option.some(JSONParserUtilities.getBigInteger(n, key));
- }
- return Option.none();
- }
-
- /**
- * @param key A key assumed to be holding a value
- * @param n A node
- * @return A big integer value from key {@code key}
- * @throws JSONParseException On type errors
- */
-
- public static BigInteger getBigInteger(
- final ObjectNode n,
- final String key)
- throws JSONParseException {
-
- NullCheck.notNull(n);
- NullCheck.notNull(key);
-
- final JsonNode v = JSONParserUtilities.getNode(n, key);
- switch (v.getNodeType()) {
- case ARRAY:
- case BINARY:
- case BOOLEAN:
- case MISSING:
- case NULL:
- case OBJECT:
- case POJO:
- case STRING: {
- final StringBuilder sb = new StringBuilder(128);
- sb.append("Expected: A key '");
- sb.append(key);
- sb.append("' with a value of type Integer\n");
- sb.append("Got: A value of type ");
- sb.append(v.getNodeType());
- sb.append("\n");
- final String m = NullCheck.notNull(sb.toString());
- throw new JSONParseException(m);
- }
- case NUMBER: {
- try {
- return new BigInteger(v.asText());
- } catch (final NumberFormatException e) {
- throw new JSONParseException(e);
- }
- }
- }
-
- throw new UnreachableCodeException();
- }
-}
diff --git a/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.kt b/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.kt
new file mode 100644
index 000000000..68733e854
--- /dev/null
+++ b/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.kt
@@ -0,0 +1,864 @@
+package org.nypl.simplified.json.core
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.node.ArrayNode
+import com.fasterxml.jackson.databind.node.JsonNodeType
+import com.fasterxml.jackson.databind.node.ObjectNode
+import com.io7m.jfunctional.None
+import com.io7m.jfunctional.Option
+import com.io7m.jfunctional.OptionType
+import com.io7m.jfunctional.OptionVisitorType
+import com.io7m.jfunctional.Some
+import org.joda.time.DateTime
+import org.joda.time.format.ISODateTimeFormat
+import java.math.BigInteger
+import java.net.URI
+import java.net.URISyntaxException
+import java.util.Objects
+
+/**
+ * Utility functions for deserializing elements from JSON.
+ *
+ * The functions take a strict approach: Types are checked upon key retrieval
+ * and exceptions are raised if the type is not exactly as expected.
+ */
+object JSONParserUtilities {
+
+ /**
+ * Check that `n` is an object.
+ *
+ * @param key An optional advisory key to be used in error messages
+ * @param n A node
+ * @return `n` as an [ObjectNode]
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun checkObject(
+ key: String?,
+ n: JsonNode
+ ): ObjectNode {
+ return when (n.nodeType) {
+ JsonNodeType.ARRAY,
+ JsonNodeType.BINARY,
+ JsonNodeType.BOOLEAN,
+ JsonNodeType.MISSING,
+ JsonNodeType.NULL,
+ JsonNodeType.NUMBER,
+ JsonNodeType.POJO,
+ JsonNodeType.STRING -> {
+ val sb = StringBuilder(128)
+ if (key != null) {
+ sb.append("Expected: A key '")
+ sb.append(key)
+ sb.append("' with a value of type Object\n")
+ sb.append("Got: A value of type ")
+ sb.append(n.nodeType)
+ sb.append("\n")
+ } else {
+ sb.append("Expected: A value of type Object\n")
+ sb.append("Got: A value of type ")
+ sb.append(n.nodeType)
+ sb.append("\n")
+ }
+ throw JSONParseException(sb.toString())
+ }
+
+ JsonNodeType.OBJECT -> {
+ n as ObjectNode
+ }
+ }
+ }
+
+ /**
+ * Check that `n` is an array.
+ *
+ * @param key An optional advisory key to be used in error messages
+ * @param n A node
+ * @return `n` as an [ObjectNode]
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun checkArray(
+ key: String?,
+ n: JsonNode
+ ): ArrayNode {
+ return when (n.nodeType) {
+ JsonNodeType.ARRAY -> {
+ n as ArrayNode
+ }
+
+ JsonNodeType.BINARY,
+ JsonNodeType.BOOLEAN,
+ JsonNodeType.MISSING,
+ JsonNodeType.NULL,
+ JsonNodeType.NUMBER,
+ JsonNodeType.POJO,
+ JsonNodeType.OBJECT,
+ JsonNodeType.STRING -> {
+ val sb = StringBuilder(128)
+ if (key != null) {
+ sb.append("Expected: A key '")
+ sb.append(key)
+ sb.append("' with a value of type Object\n")
+ sb.append("Got: A value of type ")
+ sb.append(n.nodeType)
+ sb.append("\n")
+ } else {
+ sb.append("Expected: A value of type Object\n")
+ sb.append("Got: A value of type ")
+ sb.append(n.nodeType)
+ sb.append("\n")
+ }
+ throw JSONParseException(sb.toString())
+ }
+ }
+ }
+
+ /**
+ * Check that `n` is a string.
+ *
+ * @param n A node
+ * @return `n` as a String
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun checkString(
+ n: JsonNode
+ ): String {
+ return when (n.nodeType) {
+ JsonNodeType.STRING -> {
+ n.asText()
+ }
+
+ JsonNodeType.ARRAY,
+ JsonNodeType.BINARY,
+ JsonNodeType.BOOLEAN,
+ JsonNodeType.MISSING,
+ JsonNodeType.NULL,
+ JsonNodeType.NUMBER,
+ JsonNodeType.POJO,
+ JsonNodeType.OBJECT -> {
+ val sb = StringBuilder(128)
+ sb.append("Expected: A value of type String\n")
+ sb.append("Got: A value of type ")
+ sb.append(n.nodeType)
+ sb.append("\n")
+ throw JSONParseException(sb.toString())
+ }
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param s A node
+ * @return An array from key `key`
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getArray(
+ s: ObjectNode,
+ key: String
+ ): ArrayNode {
+ val n = getNode(s, key)
+ return when (n.nodeType) {
+ JsonNodeType.ARRAY -> {
+ n as ArrayNode
+ }
+
+ JsonNodeType.BINARY,
+ JsonNodeType.BOOLEAN,
+ JsonNodeType.MISSING,
+ JsonNodeType.NULL,
+ JsonNodeType.NUMBER,
+ JsonNodeType.POJO,
+ JsonNodeType.STRING,
+ JsonNodeType.OBJECT -> {
+ val sb = StringBuilder(128)
+ sb.append("Expected: A key '")
+ sb.append(key)
+ sb.append("' with a value of type Array\n")
+ sb.append("Got: A value of type ")
+ sb.append(n.nodeType)
+ sb.append("\n")
+ throw JSONParseException(sb.toString())
+ }
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param s A node
+ * @return An array from key `key`, or null if the key is not present
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getArrayOrNull(
+ s: ObjectNode,
+ key: String
+ ): ArrayNode? {
+ return if (s.has(key)) {
+ getArray(s, key)
+ } else {
+ null
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param o A node
+ * @return A boolean value from key `key`
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getBoolean(
+ o: ObjectNode,
+ key: String
+ ): Boolean {
+ val v = getNode(o, key)
+ return when (v.nodeType) {
+ JsonNodeType.ARRAY,
+ JsonNodeType.BINARY,
+ JsonNodeType.MISSING,
+ JsonNodeType.NULL,
+ JsonNodeType.OBJECT,
+ JsonNodeType.POJO,
+ JsonNodeType.STRING,
+ JsonNodeType.NUMBER -> {
+ val sb = StringBuilder(128)
+ sb.append("Expected: A key '")
+ sb.append(key)
+ sb.append("' with a value of type Boolean\n")
+ sb.append("Got: A value of type ")
+ sb.append(v.nodeType)
+ sb.append("\n")
+ throw JSONParseException(sb.toString())
+ }
+
+ JsonNodeType.BOOLEAN -> {
+ v.asBoolean()
+ }
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return An integer value from key `key`
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getInteger(
+ n: ObjectNode,
+ key: String
+ ): Int {
+ val v = getNode(n, key)
+ return when (v.nodeType) {
+ JsonNodeType.ARRAY,
+ JsonNodeType.BINARY,
+ JsonNodeType.BOOLEAN,
+ JsonNodeType.MISSING,
+ JsonNodeType.NULL,
+ JsonNodeType.OBJECT,
+ JsonNodeType.POJO,
+ JsonNodeType.STRING -> {
+ val sb = StringBuilder(128)
+ sb.append("Expected: A key '")
+ sb.append(key)
+ sb.append("' with a value of type Integer\n")
+ sb.append("Got: A value of type ")
+ sb.append(v.nodeType)
+ sb.append("\n")
+ throw JSONParseException(sb.toString())
+ }
+
+ JsonNodeType.NUMBER -> {
+ v.asInt()
+ }
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return A double value from key `key`
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getDouble(
+ n: ObjectNode,
+ key: String
+ ): Double {
+ val v = getNode(n, key)
+ return when (v.nodeType) {
+ JsonNodeType.ARRAY,
+ JsonNodeType.BINARY,
+ JsonNodeType.BOOLEAN,
+ JsonNodeType.MISSING,
+ JsonNodeType.NULL,
+ JsonNodeType.OBJECT,
+ JsonNodeType.POJO,
+ JsonNodeType.STRING -> {
+ val sb = StringBuilder(128)
+ sb.append("Expected: A key '")
+ sb.append(key)
+ sb.append("' with a value of type Double\n")
+ sb.append("Got: A value of type ")
+ sb.append(v.nodeType)
+ sb.append("\n")
+ throw JSONParseException(sb.toString())
+ }
+
+ JsonNodeType.NUMBER -> {
+ v.asDouble()
+ }
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param s A node
+ * @return An arbitrary json node from key `key`
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getNode(
+ s: ObjectNode,
+ key: String
+ ): JsonNode {
+ if (s.has(key)) {
+ return s[key]
+ }
+ val sb = StringBuilder(128)
+ sb.append("Expected: A key '")
+ sb.append(key)
+ sb.append("'\n")
+ sb.append("Got: nothing\n")
+ throw JSONParseException(sb.toString())
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param s A node
+ * @return An object value from key `key`
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getObject(
+ s: ObjectNode,
+ key: String
+ ): ObjectNode {
+ val n = getNode(s, key)
+ return checkObject(key, n)
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param s A node
+ * @return An object value from key `key`, if the key exists
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getObjectOptional(
+ s: ObjectNode,
+ key: String
+ ): OptionType {
+ return if (s.has(key)) {
+ Option.some(
+ getObject(s, key)
+ )
+ } else {
+ Option.none()
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param s A node
+ * @return An object value from key `key`, if the key exists
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getObjectOrNull(
+ s: ObjectNode,
+ key: String
+ ): ObjectNode? {
+ return if (s.has(key)) {
+ getObject(s, key)
+ } else {
+ null
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param s A node
+ * @return A string value from key `key`
+ * @throws JSONParseException On type errors
+ */
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getString(
+ s: ObjectNode,
+ key: String
+ ): String {
+ val v = getNode(s, key)
+ return when (v.nodeType) {
+ JsonNodeType.ARRAY,
+ JsonNodeType.BINARY,
+ JsonNodeType.BOOLEAN,
+ JsonNodeType.MISSING,
+ JsonNodeType.NULL,
+ JsonNodeType.NUMBER,
+ JsonNodeType.OBJECT,
+ JsonNodeType.POJO -> {
+ val sb = StringBuilder(128)
+ sb.append("Expected: A key '")
+ sb.append(key)
+ sb.append("' with a value of type String\n")
+ sb.append("Got: A value of type ")
+ sb.append(v.nodeType)
+ sb.append("\n")
+ throw JSONParseException(sb.toString())
+ }
+
+ JsonNodeType.STRING -> {
+ v.asText()
+ }
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return An integer value from key `key`, if the key exists
+ * @throws JSONParseException On type errors
+ */
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getIntegerOptional(
+ n: ObjectNode,
+ key: String
+ ): OptionType {
+ return if (n.has(key)) {
+ Option.some(getInteger(n, key))
+ } else {
+ Option.none()
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return An integer value from key `key`, if the key exists
+ * @throws JSONParseException On type errors
+ */
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getIntegerOrNull(
+ n: ObjectNode,
+ key: String
+ ): Int? {
+ return if (n.has(key)) {
+ getInteger(n, key)
+ } else {
+ null
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return A string value from key `key`, if the key exists, or `default_value` otherwise.
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getIntegerDefault(
+ n: ObjectNode,
+ key: String,
+ default_value: Int
+ ): Int {
+ return getIntegerOptional(n, key).accept(
+ object : OptionVisitorType {
+ override fun none(n: None?): Int? {
+ return default_value
+ }
+
+ override fun some(s: Some?): Int? {
+ return s!!.get()
+ }
+ })!!
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return An double value from key `key`, if the key exists
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getDoubleOptional(
+ n: ObjectNode,
+ key: String
+ ): OptionType {
+ return if (n.has(key)) {
+ Option.some(getDouble(n, key))
+ } else {
+ Option.none()
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return An double value from key `key`, if the key exists, or `default_value` otherwise.
+ * @throws JSONParseException On type errors
+ */
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getDoubleDefault(
+ n: ObjectNode,
+ key: String,
+ default_value: Double
+ ): Double {
+ return getDoubleOptional(n, key).accept(
+ object : OptionVisitorType {
+ override fun none(n: None?): Double? {
+ return default_value
+ }
+
+ override fun some(s: Some?): Double? {
+ return s!!.get()
+ }
+ })!!
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return A string value from key `key`, if the key exists
+ * @throws JSONParseException On type errors
+ */
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getStringOptional(
+ n: ObjectNode,
+ key: String
+ ): OptionType {
+ return if (n.has(key)) {
+ if (n[key].isNull) {
+ Option.none()
+ } else {
+ Option.some(getString(n, key))
+ }
+ } else {
+ Option.none()
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return A string value from key `key`, if the key exists
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getStringOrNull(
+ n: ObjectNode,
+ key: String
+ ): String? {
+ return if (n.has(key)) {
+ if (n[key].isNull) {
+ null
+ } else {
+ getString(n, key)
+ }
+ } else {
+ null
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return A string value from key `key`, if the key exists, or `default_value` otherwise.
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getStringDefault(
+ n: ObjectNode,
+ key: String,
+ default_value: String
+ ): String {
+ return getStringOptional(n, key).accept(
+ object : OptionVisitorType {
+ override fun none(n: None?): String? {
+ return default_value
+ }
+
+ override fun some(s: Some?): String? {
+ return s!!.get()
+ }
+ })!!
+ }
+
+ /**
+ * @param s A node
+ * @param key A key assumed to be holding a value
+ * @return A timestamp value from key `key`
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getTimestamp(
+ s: ObjectNode,
+ key: String
+ ): DateTime {
+ return try {
+ ISODateTimeFormat.dateTimeParser()
+ .withZoneUTC()
+ .parseDateTime(getString(s, key))
+ } catch (e: IllegalArgumentException) {
+ throw JSONParseException(
+ String.format(
+ "Could not parse RFC3999 date for key '%s'",
+ key
+ ), e
+ )
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return A timestamp value from key `key`, if the key exists
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getTimestampOptional(
+ n: ObjectNode,
+ key: String
+ ): OptionType {
+ return if (n.has(key)) {
+ Option.some(
+ getTimestamp(
+ n,
+ key
+ )
+ )
+ } else {
+ Option.none()
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return A URI value from key `key`, if the key exists
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getURIOptional(
+ n: ObjectNode,
+ key: String
+ ): OptionType {
+ return getStringOptional(n, key).mapPartial { x: String? ->
+ try {
+ return@mapPartial URI(x)
+ } catch (e: URISyntaxException) {
+ throw JSONParseException(e)
+ }
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return A URI value from key `key`, if the key exists
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getURIOrNull(
+ n: ObjectNode,
+ key: String
+ ): URI? {
+ val opt = getURIOptional(n, key)
+ return if (opt.isSome) {
+ (opt as Some).get()
+ } else {
+ null
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return A URI value from key `key`
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getURI(
+ n: ObjectNode,
+ key: String
+ ): URI {
+ return try {
+ URI(getString(n, key).trim { it <= ' ' })
+ } catch (e: URISyntaxException) {
+ throw JSONParseException(e)
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return A URI value from key `key`
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getURIDefault(
+ n: ObjectNode,
+ key: String,
+ default_value: URI
+ ): URI {
+ Objects.requireNonNull(default_value, "Default")
+ return getURIOptional(n, key).accept(object : OptionVisitorType {
+ override fun none(n: None?): URI? {
+ return default_value
+ }
+
+ override fun some(s: Some?): URI? {
+ return s!!.get()
+ }
+ })!!
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @param v A default value
+ * @return A boolean from key `key`, or `v` if the key does not
+ * exist
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getBooleanDefault(
+ n: ObjectNode,
+ key: String,
+ v: Boolean
+ ): Boolean {
+ return if (n.has(key)) {
+ getBoolean(n, key)
+ } else {
+ v
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param n A node
+ * @return A big integer value from key `key`
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getBigInteger(
+ n: ObjectNode,
+ key: String
+ ): BigInteger {
+ val v = getNode(n, key)
+ return when (v.nodeType) {
+ JsonNodeType.ARRAY,
+ JsonNodeType.BINARY,
+ JsonNodeType.BOOLEAN,
+ JsonNodeType.MISSING,
+ JsonNodeType.NULL,
+ JsonNodeType.OBJECT,
+ JsonNodeType.POJO,
+ JsonNodeType.STRING -> {
+ val sb = StringBuilder(128)
+ sb.append("Expected: A key '")
+ sb.append(key)
+ sb.append("' with a value of type Integer\n")
+ sb.append("Got: A value of type ")
+ sb.append(v.nodeType)
+ sb.append("\n")
+ throw JSONParseException(sb.toString())
+ }
+
+ JsonNodeType.NUMBER -> {
+ try {
+ BigInteger(v.asText())
+ } catch (e: NumberFormatException) {
+ throw JSONParseException(e)
+ }
+ }
+ }
+ }
+
+ /**
+ * @param key A key assumed to be holding a value
+ * @param node A node
+ * @param defaultValue The default value
+ * @return A big integer value from key `key` or `defaultValue`
+ * @throws JSONParseException On type errors
+ */
+
+ @JvmStatic
+ @Throws(JSONParseException::class)
+ fun getBigIntegerDefault(
+ node: ObjectNode,
+ key: String,
+ defaultValue: BigInteger
+ ): BigInteger {
+ return if (node.has(key)) {
+ getBigInteger(node, key)
+ } else {
+ defaultValue
+ }
+ }
+}
diff --git a/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.java b/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.java
deleted file mode 100644
index add93985f..000000000
--- a/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package org.nypl.simplified.json.core;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.ObjectWriter;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.io7m.jnull.NullCheck;
-import com.io7m.junreachable.UnreachableCodeException;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-/**
- * Utilities for implementing JSON serializers.
- */
-
-public final class JSONSerializerUtilities
-{
- private JSONSerializerUtilities()
- {
- throw new UnreachableCodeException();
- }
-
- /**
- * Serialize the given object node to the given stream.
- *
- * @param d The node
- * @param os The output stream
- *
- * @throws IOException On I/O errors
- */
-
- public static void serialize(
- final ObjectNode d,
- final OutputStream os)
- throws IOException
- {
- NullCheck.notNull(d);
- NullCheck.notNull(os);
-
- final ObjectMapper jom = new ObjectMapper();
- final ObjectWriter jw = jom.writerWithDefaultPrettyPrinter();
- jw.writeValue(os, d);
- }
-
- /**
- * Serialize the given object node to a string.
- *
- * @param d The node
- *
- * @return Pretty-printed JSON
- *
- * @throws IOException On I/O errors
- */
-
- public static String serializeToString(
- final ObjectNode d)
- throws IOException
- {
- NullCheck.notNull(d);
-
- final ObjectMapper jom = new ObjectMapper();
- final ObjectWriter jw = jom.writerWithDefaultPrettyPrinter();
- return jw.writeValueAsString(d);
- }
-}
diff --git a/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.kt b/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.kt
new file mode 100644
index 000000000..20ec6fffd
--- /dev/null
+++ b/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.kt
@@ -0,0 +1,76 @@
+package org.nypl.simplified.json.core
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import java.io.IOException
+import java.io.OutputStream
+
+/**
+ * Utilities for implementing JSON serializers.
+ */
+object JSONSerializerUtilities {
+
+ private val objectMapper: ObjectMapper = ObjectMapper()
+
+ /**
+ * Serialize the given object node to the given stream.
+ *
+ * @param d The node
+ * @param os The output stream
+ *
+ * @throws IOException On I/O errors
+ */
+
+ @JvmStatic
+ @Throws(IOException::class)
+ fun serialize(
+ d: ObjectNode,
+ os: OutputStream
+ ) {
+ this.objectMapper.writerWithDefaultPrettyPrinter()
+ .writeValue(os, d)
+ }
+
+ /**
+ * Serialize the given object node to a string.
+ *
+ * @param d The node
+ *
+ * @return Pretty-printed JSON
+ *
+ * @throws IOException On I/O errors
+ */
+
+ @JvmStatic
+ @Throws(IOException::class)
+ fun serializeToString(
+ d: ObjectNode
+ ): String {
+ return this.objectMapper.writerWithDefaultPrettyPrinter()
+ .writeValueAsString(d)
+ }
+
+ /**
+ * Serialize the given object node to a string.
+ *
+ * @param d The node
+ *
+ * @return Pretty-printed JSON
+ *
+ * @throws IOException On I/O errors
+ */
+
+ @JvmStatic
+ @Throws(IOException::class)
+ fun serializeToString(
+ d: List
+ ): String {
+ val a = this.objectMapper.createArrayNode()
+ for (o in d) {
+ a.add(o)
+ }
+ return this.objectMapper.writerWithDefaultPrettyPrinter()
+ .writeValueAsString(a)
+ }
+}
diff --git a/simplified-lcp/src/main/java/org/nypl/simplified/lcp/LCPContentProtectionProvider.kt b/simplified-lcp/src/main/java/org/nypl/simplified/lcp/LCPContentProtectionProvider.kt
index 7ecd2f3da..eaa05b468 100644
--- a/simplified-lcp/src/main/java/org/nypl/simplified/lcp/LCPContentProtectionProvider.kt
+++ b/simplified-lcp/src/main/java/org/nypl/simplified/lcp/LCPContentProtectionProvider.kt
@@ -8,6 +8,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.librarysimplified.lcp.R
+import org.nypl.drm.core.ContentProtectionProvider
import org.readium.r2.lcp.LcpAuthenticating
import org.readium.r2.lcp.LcpService
import org.readium.r2.shared.publication.protection.ContentProtection
@@ -15,7 +16,6 @@ import org.readium.r2.shared.util.asset.AssetRetriever
import org.readium.r2.shared.util.downloads.foreground.ForegroundDownloadManager
import org.readium.r2.shared.util.http.DefaultHttpClient
import org.slf4j.LoggerFactory
-import org.nypl.drm.core.ContentProtectionProvider
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
diff --git a/simplified-main/src/main/java/org/librarysimplified/main/MainAdobeWarnings.kt b/simplified-main/src/main/java/org/librarysimplified/main/MainAdobeWarnings.kt
index 5b9645e31..e9b270014 100644
--- a/simplified-main/src/main/java/org/librarysimplified/main/MainAdobeWarnings.kt
+++ b/simplified-main/src/main/java/org/librarysimplified/main/MainAdobeWarnings.kt
@@ -4,8 +4,8 @@ import android.content.Context
import androidx.annotation.UiThread
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.librarysimplified.services.api.ServiceDirectoryType
-import org.nypl.simplified.adobe.extensions.AdobeDRMServices
import org.nypl.drm.core.AdobeAdeptExecutorType
+import org.nypl.simplified.adobe.extensions.AdobeDRMServices
/**
* Functions to display Adobe DRM related warnings.
diff --git a/simplified-main/src/main/java/org/librarysimplified/main/MainFragmentListenerDelegate.kt b/simplified-main/src/main/java/org/librarysimplified/main/MainFragmentListenerDelegate.kt
index e669835f7..64ad1c241 100644
--- a/simplified-main/src/main/java/org/librarysimplified/main/MainFragmentListenerDelegate.kt
+++ b/simplified-main/src/main/java/org/librarysimplified/main/MainFragmentListenerDelegate.kt
@@ -692,7 +692,7 @@ internal class MainFragmentListenerDelegate(
)
Viewers.openViewer(
- activity = this.fragment.requireActivity(),
+ context = MainApplication.application,
preferences = viewerPreferences,
book = book,
format = format
diff --git a/simplified-opds-auth-document/src/main/java/org/nypl/simplified/opds/auth_document/AuthenticationDocumentParser.kt b/simplified-opds-auth-document/src/main/java/org/nypl/simplified/opds/auth_document/AuthenticationDocumentParser.kt
index 67011c732..00ede1bbc 100644
--- a/simplified-opds-auth-document/src/main/java/org/nypl/simplified/opds/auth_document/AuthenticationDocumentParser.kt
+++ b/simplified-opds-auth-document/src/main/java/org/nypl/simplified/opds/auth_document/AuthenticationDocumentParser.kt
@@ -270,7 +270,7 @@ internal class AuthenticationDocumentParser(
private fun parseInput(
fieldName: String,
- root: ObjectNode?
+ root: ObjectNode
): AuthenticationObjectNYPLInput? {
return try {
AuthenticationObjectNYPLInput(
diff --git a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAcquisitionFeedEntryParser.java b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAcquisitionFeedEntryParser.java
index d1d8cc417..755d27404 100644
--- a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAcquisitionFeedEntryParser.java
+++ b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAcquisitionFeedEntryParser.java
@@ -1,5 +1,24 @@
package org.nypl.simplified.opds.core;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.ACQUISITION_URI_PREFIX_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.ALTERNATE_REL_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.ANNOTATION_URI_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.ATOM_URI;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.BIBFRAME_URI;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.CIRCULATION_ANALYTICS_OPEN_BOOK_REL_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.DUBLIN_CORE_TERMS_URI;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.GROUP_REL_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.IMAGE_URI_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.ISSUES_REL_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.LCP_URI;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.OPDS_URI;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.PREVIEW_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.RELATED_REL_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.REVOKE_URI_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.SAMPLE_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.THUMBNAIL_URI_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.TIME_TRACKING_URI_TEXT;
+
import com.io7m.jfunctional.Option;
import com.io7m.jfunctional.OptionType;
import com.io7m.jfunctional.Some;
@@ -34,25 +53,6 @@
import one.irradia.mime.api.MIMEType;
import one.irradia.mime.vanilla.MIMEParser;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.ACQUISITION_URI_PREFIX_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.ALTERNATE_REL_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.ANNOTATION_URI_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.ATOM_URI;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.BIBFRAME_URI;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.CIRCULATION_ANALYTICS_OPEN_BOOK_REL_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.DUBLIN_CORE_TERMS_URI;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.GROUP_REL_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.IMAGE_URI_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.ISSUES_REL_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.LCP_URI;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.OPDS_URI;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.PREVIEW_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.RELATED_REL_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.REVOKE_URI_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.SAMPLE_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.THUMBNAIL_URI_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.TIME_TRACKING_URI_TEXT;
-
/**
* The default implementation of the {@link OPDSAcquisitionFeedEntryParserType}
* type.
diff --git a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAtom.java b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAtom.java
index 64c85d676..0c8391690 100644
--- a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAtom.java
+++ b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAtom.java
@@ -5,7 +5,6 @@
import com.io7m.junreachable.UnreachableCodeException;
import org.joda.time.DateTime;
-import org.joda.time.format.ISODateTimeFormat;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
diff --git a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSFeedParser.java b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSFeedParser.java
index af6d5be23..74f9d2830 100644
--- a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSFeedParser.java
+++ b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSFeedParser.java
@@ -1,5 +1,12 @@
package org.nypl.simplified.opds.core;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.ATOM_URI;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.AUTHENTICATION_DOCUMENT_RELATION_URI_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.DRM_URI;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.FACET_URI_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.OPDS_URI_TEXT;
+import static org.nypl.simplified.opds.core.OPDSFeedConstants.SIMPLIFIED_URI_TEXT;
+
import com.google.common.base.Preconditions;
import com.io7m.jfunctional.Option;
import com.io7m.jfunctional.OptionType;
@@ -29,13 +36,6 @@
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.ATOM_URI;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.AUTHENTICATION_DOCUMENT_RELATION_URI_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.DRM_URI;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.FACET_URI_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.OPDS_URI_TEXT;
-import static org.nypl.simplified.opds.core.OPDSFeedConstants.SIMPLIFIED_URI_TEXT;
-
/**
* The default implementation of the {@link OPDSFeedParserType}.
*
diff --git a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSSearchParser.java b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSSearchParser.java
index 3654d119b..5ebafc86f 100644
--- a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSSearchParser.java
+++ b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSSearchParser.java
@@ -1,6 +1,7 @@
package org.nypl.simplified.opds.core;
import com.io7m.jnull.NullCheck;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.DOMException;
@@ -9,14 +10,15 @@
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.concurrent.TimeUnit;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
/**
* The default implementation of the {@link OPDSSearchParserType}.
*/
diff --git a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSXML.java b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSXML.java
index 86227a9b1..99de23083 100644
--- a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSXML.java
+++ b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSXML.java
@@ -6,7 +6,6 @@
import com.io7m.junreachable.UnreachableCodeException;
import org.joda.time.DateTime;
-import org.joda.time.format.ISODateTimeFormat;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
diff --git a/simplified-opds2-irradia/src/main/java/org/nypl/simplified/opds2/irradia/OPDS2ParsersIrradia.kt b/simplified-opds2-irradia/src/main/java/org/nypl/simplified/opds2/irradia/OPDS2ParsersIrradia.kt
index caa5ccb0c..7b222c13d 100644
--- a/simplified-opds2-irradia/src/main/java/org/nypl/simplified/opds2/irradia/OPDS2ParsersIrradia.kt
+++ b/simplified-opds2-irradia/src/main/java/org/nypl/simplified/opds2/irradia/OPDS2ParsersIrradia.kt
@@ -1,9 +1,9 @@
package org.nypl.simplified.opds2.irradia
import one.irradia.opds2_0.parser.extension.library_simplified.OPDS20CatalogExtension
+import one.irradia.opds2_0.parser.extension.spi.OPDS20ExtensionType
import one.irradia.opds2_0.parser.vanilla.OPDS20FeedParsers
import org.nypl.simplified.opds2.OPDS2Feed
-import one.irradia.opds2_0.parser.extension.spi.OPDS20ExtensionType
import org.nypl.simplified.opds2.irradia.internal.OPDS2ParserIrradia
import org.nypl.simplified.opds2.parser.api.OPDS2ParsersType
import org.nypl.simplified.parser.api.ParserType
diff --git a/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseError.kt b/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseError.kt
index 0221e293d..c31e069a9 100644
--- a/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseError.kt
+++ b/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseError.kt
@@ -1,7 +1,6 @@
package org.nypl.simplified.parser.api
import java.io.Serializable
-import java.lang.Exception
import java.net.URI
/**
diff --git a/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseWarning.kt b/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseWarning.kt
index b83a53b30..2265ed56b 100644
--- a/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseWarning.kt
+++ b/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseWarning.kt
@@ -1,7 +1,6 @@
package org.nypl.simplified.parser.api
import java.io.Serializable
-import java.lang.Exception
import java.net.URI
/**
diff --git a/simplified-profiles/src/main/java/org/nypl/simplified/profiles/ProfileDescriptionJSON.kt b/simplified-profiles/src/main/java/org/nypl/simplified/profiles/ProfileDescriptionJSON.kt
index 2aadf7b9a..379503be9 100644
--- a/simplified-profiles/src/main/java/org/nypl/simplified/profiles/ProfileDescriptionJSON.kt
+++ b/simplified-profiles/src/main/java/org/nypl/simplified/profiles/ProfileDescriptionJSON.kt
@@ -348,7 +348,7 @@ object ProfileDescriptionJSON {
private fun deserializeReaderPreferences(
objectMapper: ObjectMapper,
- node: ObjectNode?
+ node: ObjectNode
): ReaderPreferences {
return JSONParserUtilities.getObjectOptional(node, "readerPreferences")
.mapPartial { prefsNode ->
@@ -367,11 +367,10 @@ object ProfileDescriptionJSON {
private fun deserializePlaybackRates(
objectMapper: ObjectMapper,
- node: ObjectNode?
+ node: ObjectNode
): Map {
- val str = JSONParserUtilities.getObjectOrNull(
- node, "playbackRates"
- ) ?: return hashMapOf()
+ val str = JSONParserUtilities.getObjectOrNull(node, "playbackRates")
+ ?: return hashMapOf()
val map = objectMapper.readValue(
str.toString(),
@@ -385,34 +384,6 @@ object ProfileDescriptionJSON {
}
}
- private fun deserializeSleepTimers(
- objectMapper: ObjectMapper,
- node: ObjectNode?
- ): Map {
- val str = JSONParserUtilities.getObjectOrNull(
- node, "sleepTimers"
- ) ?: return hashMapOf()
-
- val map = objectMapper.readValue(
- str.toString(),
- object : TypeReference