Skip to content

Commit c288710

Browse files
authored
Merge pull request #622 from NYPL-Simplified/SIMPLY-1531
Be more careful about book content types
2 parents a6565d9 + 63a24a9 commit c288710

5 files changed

Lines changed: 486 additions & 33 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.nypl.simplified.books.core
2+
3+
/**
4+
* An exception indicating that a book cannot be fulfilled because the server
5+
* delivered an unexpected content type.
6+
*
7+
* @param message The exception message
8+
* @param expected The expected types
9+
* @param received The received type
10+
*/
11+
12+
class BookUnexpectedTypeException(
13+
message: String,
14+
val expected: Set<String>,
15+
val received: String) : BookException(message)

simplified-books-core/src/main/java/org/nypl/simplified/books/core/BooksControllerBorrowTask.kt

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.nypl.simplified.books.core
22

3+
import com.google.common.base.Preconditions
34
import com.io7m.jfunctional.None
45
import com.io7m.jfunctional.Option
56
import com.io7m.jfunctional.OptionType
@@ -205,7 +206,9 @@ internal class BooksControllerBorrowTask(
205206
}
206207

207208
@Throws(IOException::class)
208-
private fun runFulfillSimplifiedBearerToken(file: File) {
209+
private fun runFulfillSimplifiedBearerToken(
210+
acquisition: OPDSAcquisition,
211+
file: File) {
209212
LOG.debug("[{}]: fulfilling Simplified bearer token file", this.shortID)
210213

211214
/*
@@ -231,14 +234,15 @@ internal class BooksControllerBorrowTask(
231234
return
232235
}
233236

234-
val acquisition = OPDSAcquisition(
235-
ACQUISITION_GENERIC,
236-
token.location,
237-
Option.some(BooksControllerBorrowTask.SIMPLIFIED_BEARER_TOKEN_CONTENT_TYPE),
238-
emptyList())
237+
val nextAcquisition =
238+
OPDSAcquisition(
239+
ACQUISITION_GENERIC,
240+
token.location,
241+
acquisition.type,
242+
acquisition.indirectAcquisitions)
239243

240244
val auth = HTTPAuthOAuth(token.accessToken)
241-
this.runAcquisitionFulfillDoDownload(acquisition, Option.some(auth))
245+
this.runAcquisitionFulfillDoDownload(nextAcquisition, Option.some(auth))
242246
}
243247

244248
private fun downloadDataReceived(
@@ -609,7 +613,7 @@ internal class BooksControllerBorrowTask(
609613
}
610614

611615
private fun runAcquisitionFulfillDoDownload(
612-
a: OPDSAcquisition,
616+
acquisition: OPDSAcquisition,
613617
predeterminedAuth: OptionType<HTTPAuthType>): DownloadType {
614618

615619
/*
@@ -622,8 +626,9 @@ internal class BooksControllerBorrowTask(
622626
}
623627

624628
val sid = this.shortID
625-
this.fulfillURI = a.uri
626-
LOG.debug("[{}]: starting download {}", sid, a.uri)
629+
this.fulfillURI = acquisition.uri
630+
LOG.debug("[{}]: starting download {}", sid, acquisition.uri)
631+
LOG.debug("[{}]: expecting content type {}", sid, acquisition.type)
627632

628633
/*
629634
* Point the downloader at the acquisition link. The result will be an
@@ -634,7 +639,7 @@ internal class BooksControllerBorrowTask(
634639
*/
635640

636641
val task = this
637-
return this.downloader.download(a.uri, auth, object : DownloadListenerType {
642+
return this.downloader.download(acquisition.uri, auth, object : DownloadListenerType {
638643
override fun onDownloadStarted(
639644
download: DownloadType,
640645
expectedTotal: Long) {
@@ -689,20 +694,15 @@ internal class BooksControllerBorrowTask(
689694
if (BooksControllerBorrowTask.ACSM_CONTENT_TYPE == contentType) {
690695
task.runFulfillACSM(file)
691696
} else if (BooksControllerBorrowTask.SIMPLIFIED_BEARER_TOKEN_CONTENT_TYPE == contentType) {
692-
task.runFulfillSimplifiedBearerToken(file)
697+
task.runFulfillSimplifiedBearerToken(acquisition, file)
693698
} else {
694699
task.saveFinalContent(
695700
file = file,
696-
contentType = contentType)
701+
expectedContentTypes = acquisition.availableFinalContentTypes(),
702+
receivedContentType = contentType)
697703
}
698-
} catch (e: IOException) {
699-
LOG.error("onDownloadCompleted: i/o exception: ", e)
700-
task.downloadFailed(Option.some(e))
701-
} catch (e: BookUnsupportedTypeException) {
702-
LOG.error("onDownloadCompleted: unsupported book exception: ", e)
703-
task.downloadFailed(Option.some(e))
704-
} catch (e: AdobeAdeptACSMException) {
705-
LOG.error("onDownloadCompleted: acsm exception: ", e)
704+
} catch (e: Exception) {
705+
LOG.error("onDownloadCompleted: exception: ", e)
706706
task.downloadFailed(Option.some(e))
707707
}
708708
}
@@ -711,14 +711,23 @@ internal class BooksControllerBorrowTask(
711711

712712
private fun saveFinalContent(
713713
file: File,
714-
contentType: String) {
714+
expectedContentTypes: Set<String>,
715+
receivedContentType: String) {
716+
717+
LOG.debug(
718+
"[{}]: saving content {} (expected one of {}, received {})",
719+
this.shortID,
720+
file,
721+
expectedContentTypes,
722+
receivedContentType)
715723

716-
LOG.debug("[{}]: saving content {} ({})", this.shortID, file, contentType)
717724
LOG.debug("[{}]: saving adobe rights {}", this.shortID, this.adobeRights)
718725
LOG.debug("[{}]: saving fulfill URI {}", this.shortID, this.fulfillURI)
719726

727+
val handleContentType =
728+
checkExpectedContentType(expectedContentTypes, receivedContentType)
720729
val formatHandleOpt: OptionType<BookDatabaseEntryFormatHandle> =
721-
this.databaseEntry.entryFindFormatHandleForContentType(contentType)
730+
this.databaseEntry.entryFindFormatHandleForContentType(handleContentType)
722731

723732
fun updateStatus() {
724733
val downloadedSnap = this.databaseEntry.entryGetSnapshot()
@@ -747,8 +756,55 @@ internal class BooksControllerBorrowTask(
747756
}
748757
} else {
749758
LOG.error("[{}]: database entry does not have a format handle for {}",
750-
this.shortID, contentType)
751-
throw BookUnsupportedTypeException(contentType)
759+
this.shortID, handleContentType)
760+
throw BookUnsupportedTypeException(handleContentType)
761+
}
762+
}
763+
764+
/*
765+
* If we expect a specific content type, but the server actually delivers application/octet-stream,
766+
* then assume that the server delivered the expected type. Otherwise, check that the received
767+
* type matches the expected type.
768+
*/
769+
770+
private fun checkExpectedContentType(
771+
expectedContentTypes: Set<String>,
772+
receivedContentType: String): String {
773+
774+
Preconditions.checkArgument(
775+
!expectedContentTypes.isEmpty(),
776+
"At least one expected content type")
777+
778+
return when (receivedContentType) {
779+
"application/octet-stream" -> {
780+
LOG.debug("[{}]: expected one of {} but received {} (acceptable)",
781+
this.shortID,
782+
expectedContentTypes,
783+
receivedContentType)
784+
expectedContentTypes.first()
785+
}
786+
787+
else -> {
788+
if (expectedContentTypes.contains(receivedContentType)) {
789+
return receivedContentType
790+
}
791+
792+
LOG.debug("[{}]: expected {} but received {} (unacceptable)",
793+
this.shortID, expectedContentTypes, receivedContentType)
794+
795+
throw BookUnexpectedTypeException(
796+
message =
797+
StringBuilder("Unexpected content type\n")
798+
.append(" Expected: One of ")
799+
.append(expectedContentTypes)
800+
.append('\n')
801+
.append(" Received: ")
802+
.append(receivedContentType)
803+
.append('\n')
804+
.toString(),
805+
expected = expectedContentTypes,
806+
received = receivedContentType)
807+
}
752808
}
753809
}
754810

@@ -808,7 +864,10 @@ internal class BooksControllerBorrowTask(
808864
override fun onFulfillmentSuccess(file: File, loan: AdobeAdeptLoan) {
809865
try {
810866
this.task.adobeRights = Option.some(loan)
811-
this.task.saveFinalContent(file, this.contentType)
867+
this.task.saveFinalContent(
868+
file,
869+
expectedContentTypes = setOf(this.contentType),
870+
receivedContentType = this.contentType)
812871
} catch (x: Throwable) {
813872
LOG.error("failure saving content/rights: ", x)
814873
this.task.downloadFailed(Option.some(x))

0 commit comments

Comments
 (0)