Skip to content

Commit

Permalink
Use a linearized acquisition model like iOS
Browse files Browse the repository at this point in the history
This moves OPDS acquisitions to a model where a set of linearized
acquisition paths are exposed, and the app works in terms of those
paths. This allows the app to make much more intelligent choices
with regards to which acquisition to follow, and should make it
possible to borrow books from Archive.org feeds once the proper
authentication mechanisms are in place. Additionally, the OPDS parser
is no longer responsible for filtering out acquisition entries that
don't provide supported book types - this is now the responsibility
of the higher-level feed loader.

Affects: https://jira.nypl.org/browse/SIMPLY-1788
Affects: https://jira.nypl.org/browse/SIMPLY-1801
  • Loading branch information
io7m committed Mar 20, 2019
1 parent 028d2e5 commit ff7efc8
Show file tree
Hide file tree
Showing 32 changed files with 1,202 additions and 1,067 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ public static BooksType getBooks(final Account account, final Context context, f
BookDatabase.Companion.newDatabase(context, in_json_serializer, in_json_parser, books_database_directory);

final OPDSAcquisitionFeedEntryParserType in_entry_parser =
OPDSAcquisitionFeedEntryParser.newParser(BookFormats.Companion.supportedBookMimeTypes());
OPDSAcquisitionFeedEntryParser.newParser();

final OPDSFeedParserType p = OPDSFeedParser.newParser(in_entry_parser);
final OPDSSearchParserType s = OPDSSearchParser.newParser();
Expand Down Expand Up @@ -750,7 +750,7 @@ private CatalogAppServices(

this.http = HTTP.newHTTP();
final OPDSAcquisitionFeedEntryParserType in_entry_parser =
OPDSAcquisitionFeedEntryParser.newParser(BookFormats.Companion.supportedBookMimeTypes());
OPDSAcquisitionFeedEntryParser.newParser();
final OPDSJSONSerializerType in_json_serializer =
OPDSJSONSerializer.newSerializer();
final OPDSJSONParserType in_json_parser = OPDSJSONParser.newParser();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.nypl.simplified.books.core.BooksType;
import org.nypl.simplified.books.core.FeedEntryOPDS;
import org.nypl.simplified.opds.core.OPDSAcquisition;
import org.nypl.simplified.opds.core.OPDSAcquisitionPath;
import org.nypl.simplified.opds.core.OPDSAvailabilityHoldable;
import org.nypl.simplified.opds.core.OPDSAvailabilityType;

Expand All @@ -37,7 +38,7 @@ public CatalogAcquisitionButton(
final Activity in_activity,
final BooksType in_books,
final BookID in_book_id,
final OPDSAcquisition in_acq,
final OPDSAcquisitionPath in_acq,
final FeedEntryOPDS in_entry)
{
super(in_activity);
Expand All @@ -51,7 +52,7 @@ public CatalogAcquisitionButton(
final int mainColor = ContextCompat.getColor(this.getContext(), resID);
this.getTextView().setTextColor(mainColor);

switch (in_acq.getRelation()) {
switch (in_acq.getNext().getRelation()) {
case ACQUISITION_OPEN_ACCESS:
this.getTextView().setText(
NullCheck.notNull(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import com.io7m.jfunctional.OptionType;
import com.io7m.jnull.NullCheck;
import com.io7m.jnull.Nullable;
import com.io7m.junreachable.UnimplementedCodeException;
import com.io7m.junreachable.UnreachableCodeException;

import org.nypl.simplified.app.LoginActivity;
Expand All @@ -26,7 +25,8 @@
import org.nypl.simplified.books.core.FeedEntryOPDS;
import org.nypl.simplified.books.core.LogUtilities;
import org.nypl.simplified.opds.core.OPDSAcquisition;
import org.nypl.simplified.opds.core.OPDSAcquisitionFeedEntry;
import org.nypl.simplified.opds.core.OPDSAcquisitionPath;
import org.nypl.simplified.opds.core.OPDSAcquisitionRelation;
import org.slf4j.Logger;

/**
Expand All @@ -45,7 +45,7 @@ public final class CatalogAcquisitionButtonController
LOG = LogUtilities.getLog(CatalogAcquisitionButtonController.class);
}

private final OPDSAcquisition acquisition;
private final OPDSAcquisitionPath acquisition;
private final Activity activity;
private final BooksType books;
private final FeedEntryOPDS entry;
Expand All @@ -65,7 +65,7 @@ public CatalogAcquisitionButtonController(
final Activity in_activity,
final BooksType in_books,
final BookID in_id,
final OPDSAcquisition in_acq,
final OPDSAcquisitionPath in_acq,
final FeedEntryOPDS in_entry)
{
this.activity = NullCheck.notNull(in_activity);
Expand All @@ -80,7 +80,7 @@ public CatalogAcquisitionButtonController(
{
if (this.books.accountIsLoggedIn()
&& Simplified.getCurrentAccount().needsAuth()
&& this.acquisition.getRelation() != OPDSAcquisition.Relation.ACQUISITION_OPEN_ACCESS) {
&& this.acquisition.getNext().getRelation() != OPDSAcquisitionRelation.ACQUISITION_OPEN_ACCESS) {
this.books.accountGetCachedLoginDetails(
new AccountGetCachedCredentialsListenerType()
{
Expand All @@ -96,7 +96,7 @@ public CatalogAcquisitionButtonController(
}
});
} else if (!Simplified.getCurrentAccount().needsAuth()
|| this.acquisition.getRelation() == OPDSAcquisition.Relation.ACQUISITION_OPEN_ACCESS) {
|| this.acquisition.getNext().getRelation() == OPDSAcquisitionRelation.ACQUISITION_OPEN_ACCESS) {
this.getBook();
}
else {
Expand Down Expand Up @@ -144,9 +144,9 @@ private void tryLogin()
}

private void getBook() {
LOG.debug("attempting borrow of {} acquisition", this.acquisition.getRelation());
LOG.debug("attempting borrow of {} acquisition", this.acquisition.getNext().getRelation());

switch (this.acquisition.getRelation()) {
switch (this.acquisition.getNext().getRelation()) {
case ACQUISITION_BORROW:
case ACQUISITION_GENERIC:
case ACQUISITION_OPEN_ACCESS: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.nypl.simplified.books.core.BookAcquisitionSelection
import org.nypl.simplified.books.core.BooksType
import org.nypl.simplified.books.core.FeedEntryOPDS
import org.nypl.simplified.opds.core.OPDSAcquisition
import org.nypl.simplified.opds.core.OPDSAcquisitionPath
import org.slf4j.LoggerFactory

/**
Expand Down Expand Up @@ -48,8 +49,9 @@ class CatalogAcquisitionButtons private constructor() {
val bookID = entry.bookID
val opdsEntry = entry.feedEntry

val acquisitionOpt = BookAcquisitionSelection.preferredAcquisition(opdsEntry.acquisitions)
if (acquisitionOpt is Some<OPDSAcquisition>) {
val acquisitionOpt =
BookAcquisitionSelection.preferredAcquisition(opdsEntry.acquisitionPaths)
if (acquisitionOpt is Some<OPDSAcquisitionPath>) {
val acquisition = acquisitionOpt.get()
viewGroup.addView(CatalogAcquisitionButton(activity, books, bookID, acquisition, entry))
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import org.nypl.simplified.books.core.FeedEntryType
import org.nypl.simplified.books.core.LogUtilities
import org.nypl.simplified.opds.core.OPDSAcquisition
import org.nypl.simplified.opds.core.OPDSAcquisitionFeedEntry
import org.nypl.simplified.opds.core.OPDSAcquisitionPath
import org.nypl.simplified.opds.core.OPDSAvailabilityOpenAccess
import org.nypl.simplified.stack.ImmutableStack
import org.slf4j.Logger
Expand Down Expand Up @@ -323,14 +324,14 @@ class CatalogBookDetailView(
val opdsEntry =
currentEntry.feedEntry
val acquisitionOpt =
BookAcquisitionSelection.preferredAcquisition(opdsEntry.acquisitions)
BookAcquisitionSelection.preferredAcquisition(opdsEntry.acquisitionPaths)

/*
* Theoretically, if the book has ever been downloaded, then the
* acquisition list must have contained one usable acquisition relation...
*/

if (!(acquisitionOpt is Some<OPDSAcquisition>)) {
if (!(acquisitionOpt is Some<OPDSAcquisitionPath>)) {
throw UnreachableCodeException()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
import org.nypl.simplified.multilibrary.Account;
import org.nypl.simplified.opds.core.OPDSAcquisition;
import org.nypl.simplified.opds.core.OPDSAcquisitionFeedEntry;
import org.nypl.simplified.opds.core.OPDSAcquisitionPath;
import org.slf4j.Logger;

import java.util.List;
Expand Down Expand Up @@ -372,8 +373,8 @@ public void onFailure(Throwable t) {
* Manually construct an acquisition controller for the retry button.
*/

final OptionType<OPDSAcquisition> a_opt =
BookAcquisitionSelection.INSTANCE.preferredAcquisition(oe.getAcquisitions());
final OptionType<OPDSAcquisitionPath> a_opt =
BookAcquisitionSelection.INSTANCE.preferredAcquisition(oe.getAcquisitionPaths());

/*
* Theoretically, if the book has ever been downloaded, then the
Expand All @@ -384,7 +385,7 @@ public void onFailure(Throwable t) {
throw new UnreachableCodeException();
}

final OPDSAcquisition a = ((Some<OPDSAcquisition>) a_opt).get();
final OPDSAcquisitionPath a = ((Some<OPDSAcquisitionPath>) a_opt).get();
final CatalogAcquisitionButtonController retry_ctl =
new CatalogAcquisitionButtonController(
this.activity, this.books, fe.getBookID(), a, fe);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package org.nypl.simplified.books.core
import com.io7m.jfunctional.Option
import com.io7m.jfunctional.OptionType
import org.nypl.simplified.opds.core.OPDSAcquisition
import org.nypl.simplified.opds.core.OPDSAcquisition.Relation.ACQUISITION_BORROW
import org.nypl.simplified.opds.core.OPDSAcquisition.Relation.ACQUISITION_BUY
import org.nypl.simplified.opds.core.OPDSAcquisition.Relation.ACQUISITION_GENERIC
import org.nypl.simplified.opds.core.OPDSAcquisition.Relation.ACQUISITION_OPEN_ACCESS
import org.nypl.simplified.opds.core.OPDSAcquisition.Relation.ACQUISITION_SAMPLE
import org.nypl.simplified.opds.core.OPDSAcquisition.Relation.ACQUISITION_SUBSCRIBE
import org.nypl.simplified.opds.core.OPDSAcquisitionPath
import org.nypl.simplified.opds.core.OPDSAcquisitionRelation.ACQUISITION_BORROW
import org.nypl.simplified.opds.core.OPDSAcquisitionRelation.ACQUISITION_BUY
import org.nypl.simplified.opds.core.OPDSAcquisitionRelation.ACQUISITION_GENERIC
import org.nypl.simplified.opds.core.OPDSAcquisitionRelation.ACQUISITION_OPEN_ACCESS
import org.nypl.simplified.opds.core.OPDSAcquisitionRelation.ACQUISITION_SAMPLE
import org.nypl.simplified.opds.core.OPDSAcquisitionRelation.ACQUISITION_SUBSCRIBE

/**
* Functions to pick an acquisition based on the supported formats.
Expand All @@ -17,26 +18,38 @@ import org.nypl.simplified.opds.core.OPDSAcquisition.Relation.ACQUISITION_SUBSCR
object BookAcquisitionSelection {

/**
* Pick the preferred acquisition from the list of acquisitions. The selection is made
* Pick the preferred acquisition path from the list of acquisition paths. The selection is made
* based on the supported relations of the acquisitions, and the available final content
* types.
*
* @return A preferred acquisition, or nothing if none of the acquisitions were suitable
*/

fun preferredAcquisition(acquisitions: List<OPDSAcquisition>): OptionType<OPDSAcquisition> {
fun preferredAcquisition(acquisitions: List<OPDSAcquisitionPath>): OptionType<OPDSAcquisitionPath> {

val onlySupportedRelations: List<OPDSAcquisition> =
acquisitions.filter { acquisition -> relationIsSupported(acquisition) }
val onlySupportedRelations: List<OPDSAcquisitionPath> =
acquisitions.filter { acquisition -> relationIsSupported(acquisition.next) }

for (acquisition in onlySupportedRelations) {
val supportedContentTypes = BookFormats.supportedBookMimeTypes()
val availableContentTypes = acquisition.availableFinalContentTypeNames()
for (contentType in supportedContentTypes) {
if (availableContentTypes.contains(contentType)) {
return Option.some(acquisition)
for (acquisitionPath in onlySupportedRelations) {

/*
* Check that all types in the sequence except the last are borrowable, and that
* the last type in the sequence is usable as a book.
*/

val typeSequence = acquisitionPath.sequence
if (typeSequence.size == 1) {
if (BookFormats.isSupportedBookMimeType(typeSequence.first())) {
return Option.some(acquisitionPath)
}
}

val head = typeSequence.dropLast(1)
val tail = typeSequence.last()
if (head.all(BookFormats.Companion::isSupportedBorrowMimeType)
&& BookFormats.isSupportedBookMimeType(tail)) {
return Option.some(acquisitionPath)
}
}

return Option.none()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -673,11 +673,11 @@ class BookDatabase private constructor(
}

private fun lockedConfigureForEntry(entry: OPDSAcquisitionFeedEntry) {
entry.acquisitions.forEach { acquisition ->
entry.acquisitionPaths.forEach { path ->
this.owner.createFormatHandleIfRequired(
owner = this,
existingFormats = this.formatsHandles,
contentTypes = acquisition.availableFinalContentTypeNames())
contentTypes = setOf(path.finalContentType().fullType))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.nypl.simplified.books.core
import com.io7m.jfunctional.Option
import com.io7m.jfunctional.OptionType
import com.io7m.junreachable.UnreachableCodeException
import org.nypl.simplified.mime.MIMEType
import org.nypl.simplified.opds.core.OPDSAcquisitionFeedEntry

import java.util.Collections
Expand All @@ -26,6 +27,17 @@ class BookFormats private constructor() {
makeEPUBMimeTypes()
private val SUPPORTED_BOOK_MIME_TYPES =
makeSupportedBookMimeTypes(EPUB_MIME_TYPES, AUDIO_BOOK_MIME_TYPES)
private val SUPPORTED_BORROW_MIME_TYPES =
makeSupportedBorrowMimeTypes(SUPPORTED_BOOK_MIME_TYPES)

private fun makeSupportedBorrowMimeTypes(bookTypes: Set<String>): Set<String> {
val types = HashSet<String>(bookTypes.size + 4)
types.addAll(bookTypes)
types.add("application/atom+xml")
types.add("application/vnd.adobe.adept+xml")
types.add("application/vnd.librarysimplified.bearer-token+json")
return Collections.unmodifiableSet(types)
}

private fun makeEPUBMimeTypes(): Set<String> {
val types = HashSet<String>(1)
Expand Down Expand Up @@ -74,22 +86,49 @@ class BookFormats private constructor() {
return AUDIO_BOOK_MIME_TYPES
}

/**
* @return The set of formats that the application knows how to borrow. Note that this is
* distinct from the set of book formats the application supports: Supported book formats
* may be saved to disk whilst supported borrow formats include those formats via which
* the application can traverse to reach a book format that it supports.
*/

fun supportedBorrowMimeTypes(): Set<String> {
return SUPPORTED_BORROW_MIME_TYPES
}

/**
* @return `true` if the given MIME type is a supported borrow type
*/

fun isSupportedBorrowMimeType(mime: MIMEType): Boolean {
return supportedBorrowMimeTypes().contains(mime.fullType)
}

/**
* @return `true` if the given MIME type is a supported final book type
*/

fun isSupportedBookMimeType(mime: MIMEType): Boolean {
return supportedBookMimeTypes().contains(mime.fullType)
}

private val formats = BookFormatDefinition.values()

/**
* @return The probable format of the book in the given OPDS entry
*/

fun inferFormat(entry: OPDSAcquisitionFeedEntry): OptionType<BookFormatDefinition> {
for (acquisition in entry.acquisitions) {
for (path in entry.acquisitionPaths) {
for (format in this.formats) {
val formatContentTypes = format.supportedContentTypes()
val bookAvailable = acquisition.availableFinalContentTypes()
if (formatContentTypes.intersect(bookAvailable).isNotEmpty()) {
if (formatContentTypes.contains(path.finalContentType().fullType)) {
return Option.some(format)
}
}
}

return Option.none()
}
}
Expand Down
Loading

0 comments on commit ff7efc8

Please sign in to comment.