Skip to content

Commit

Permalink
Move WorkbookRepository from otter-jvm to otter-common repository.
Browse files Browse the repository at this point in the history
  • Loading branch information
aunger committed May 17, 2019
1 parent c2b69ed commit 877d2fd
Show file tree
Hide file tree
Showing 3 changed files with 808 additions and 0 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies {
//Testing
testCompile group: 'junit', name: 'junit', version: '4.12'
testCompile "org.mockito:mockito-core:2.+"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"

//resource container
implementation 'org.wycliffeassociates:kotlin-resource-container'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
package org.wycliffeassociates.otter.common.persistence.repositories

import com.jakewharton.rxrelay2.BehaviorRelay
import com.jakewharton.rxrelay2.ReplayRelay
import io.reactivex.Completable
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.plusAssign
import org.wycliffeassociates.otter.common.data.model.*
import org.wycliffeassociates.otter.common.data.model.Collection
import org.wycliffeassociates.otter.common.data.model.Take
import org.wycliffeassociates.otter.common.data.workbook.*
import java.util.*
import java.util.Collections.synchronizedMap

private typealias ModelTake = org.wycliffeassociates.otter.common.data.model.Take
private typealias WorkbookTake = org.wycliffeassociates.otter.common.data.workbook.Take

class WorkbookRepository(private val db: IDatabaseAccessors) : IWorkbookRepository {
constructor(
collectionRepository: ICollectionRepository,
contentRepository: IContentRepository,
resourceRepository: IResourceRepository,
takeRepository: ITakeRepository
) : this(
DefaultDatabaseAccessors(
collectionRepository,
contentRepository,
resourceRepository,
takeRepository
)
)

/** Disposers for Relays in the current workbook. */
private val connections = CompositeDisposable()

override fun get(source: Collection, target: Collection): Workbook {
// Clear database connections and dispose observables for the
// previous Workbook if a new one was requested.
connections.clear()
return Workbook(book(source), book(target))
}

private fun book(bookCollection: Collection): Book {
return Book(
title = bookCollection.titleKey,
sort = bookCollection.sort,
chapters = constructBookChapters(bookCollection),
subtreeResources = db.getSubtreeResourceInfo(bookCollection)
)
}

private fun constructBookChapters(bookCollection: Collection): Observable<Chapter> {
return Observable.defer {
db.getChildren(bookCollection)
.flattenAsObservable { it }
.concatMapEager { constructChapter(it).toObservable() }
}.cache()
}

private fun constructChapter(chapterCollection: Collection): Single<Chapter> {
return db.getCollectionMetaContent(chapterCollection)
.map { metaContent ->
Chapter(
title = chapterCollection.titleKey,
sort = chapterCollection.sort,
resources = constructResourceGroups(chapterCollection),
audio = constructAssociatedAudio(metaContent),
chunks = constructChunks(chapterCollection),
subtreeResources = db.getSubtreeResourceInfo(chapterCollection)
)
}
}

private fun constructChunks(chapterCollection: Collection): Observable<Chunk> {
return Observable.defer {
db.getContentByCollection(chapterCollection)
.flattenAsObservable { it }
.filter { it.labelKey == "verse" }
.map(this::chunk)
}.cache()
}

private fun chunk(content: Content) = Chunk(
title = content.start.toString(),
sort = content.sort,
audio = constructAssociatedAudio(content),
resources = constructResourceGroups(content),
text = textItem(content)
)

private fun textItem(content: Content?): TextItem? {
return content
?.format
?.let { MimeType.of(it) }
?.let { mimeType ->
content.text?.let {
TextItem(it, mimeType)
}
}
}

private fun constructResource(title: Content, body: Content?): Resource? {
val titleTextItem = textItem(title)
?: return null

return Resource(
sort = title.sort,
title = titleTextItem,
body = textItem(body),
titleAudio = constructAssociatedAudio(title),
bodyAudio = body?.let { constructAssociatedAudio(body) }
)
}

private fun constructResourceGroups(content: Content) = constructResourceGroups(
resourceInfoList = db.getResourceInfo(content),
getResourceContents = { db.getResources(content, it) }
)

private fun constructResourceGroups(collection: Collection) = constructResourceGroups(
resourceInfoList = db.getResourceInfo(collection),
getResourceContents = { db.getResources(collection, it) }
)

private fun constructResourceGroups(
resourceInfoList: List<ResourceInfo>,
getResourceContents: (ResourceInfo) -> Observable<Content>
): List<ResourceGroup> {
return resourceInfoList.map {
val resources = Observable.defer {
getResourceContents(it)
.contentsToResources()
}.cache()

ResourceGroup(it, resources)
}
}

private fun Observable<Content>.contentsToResources(): Observable<Resource> {
return this
.buffer(2, 1) // create a rolling window of size 2
.concatMapIterable { list ->
val a = list.getOrNull(0)
val b = list.getOrNull(1)
listOfNotNull(
when {
// If the first element isn't a title, skip this pair, because the body
// was already used by the previous window.
a?.labelKey != "title" -> null
// If the second element isn't a body, just use the title. (The second
// element will appear again in the next window.)
b?.labelKey != "body" -> constructResource(a, null)
// Else, we have a title/body pair, so use it.
else -> constructResource(a, b)
}
)
}
}

/** Build a relay primed with the current deletion state, that responds to updates by writing to the DB. */
private fun deletionRelay(modelTake: ModelTake): BehaviorRelay<DateHolder> {
val relay = BehaviorRelay.createDefault(DateHolder(modelTake.deleted))

val subscription = relay
.skip(1) // ignore the initial value
.subscribe {
db.updateTake(modelTake, it)
}

connections += subscription
return relay
}


private fun deselectUponDelete(take: WorkbookTake, selectedTakeRelay: BehaviorRelay<TakeHolder>) {
val subscription = take.deletedTimestamp
.filter { localDate -> localDate.value != null }
.filter { take == selectedTakeRelay.value?.value }
.map { TakeHolder(null) }
.subscribe(selectedTakeRelay)
connections += subscription
}

private fun workbookTake(modelTake: ModelTake): WorkbookTake {
return WorkbookTake(
name = modelTake.filename,
file = modelTake.path,
number = modelTake.number,
format = MimeType.WAV, // TODO
createdTimestamp = modelTake.created,
deletedTimestamp = deletionRelay(modelTake)
)
}

private fun modelTake(workbookTake: WorkbookTake, markers: List<Marker> = listOf()): ModelTake {
return ModelTake(
filename = workbookTake.file.name,
path = workbookTake.file,
number = workbookTake.number,
created = workbookTake.createdTimestamp,
deleted = null,
played = false,
markers = markers
)
}

private fun constructAssociatedAudio(content: Content): AssociatedAudio {
/** Map to recover model.Take objects from workbook.Take objects. */
val takeMap = synchronizedMap(WeakHashMap<WorkbookTake, ModelTake>())

/** The initial selected take, from the DB. */
val initialSelectedTake = TakeHolder(content.selectedTake?.let { workbookTake(it) })

/** Relay to send selected-take updates out to consumers, but also receive updates from UI. */
val selectedTakeRelay = BehaviorRelay.createDefault(initialSelectedTake)

// When we receive an update, write it to the DB.
val selectedTakeRelaySubscription = selectedTakeRelay
.distinctUntilChanged() // Don't write unless changed
.skip(1) // Don't write the value we just loaded from the DB
.subscribe {
content.selectedTake = it.value?.let { wbTake -> takeMap[wbTake] }
db.updateContent(content)
}

/** Initial Takes read from the DB. */
val takesFromDb = db.getTakeByContent(content)
.flattenAsObservable { list -> list.sortedBy { it.number } }
.map { workbookTake(it) to it }

/** Relay to send Takes out to consumers, but also receive new Takes from UI. */
val takesRelay = ReplayRelay.create<WorkbookTake>()
takesFromDb
// Record the mapping between data types.
.doOnNext { (wbTake, modelTake) -> takeMap[wbTake] = modelTake }
// Feed the initial list to takesRelay
.map { (wbTake, _) -> wbTake }
.subscribe(takesRelay)

val takesRelaySubscription = takesRelay
// When the selected take becomes deleted, deselect it.
.doOnNext { deselectUponDelete(it, selectedTakeRelay) }

// Keep the takeMap current.
.filter { !takeMap.contains(it) } // don't duplicate takes
.map { it to modelTake(it) }
.doOnNext { (wbTake, modelTake) -> takeMap[wbTake] = modelTake }

// Insert the new take into the DB.
.subscribe { (_, modelTake) ->
db.insertTakeForContent(modelTake, content)
.subscribe { insertionId -> modelTake.id = insertionId }
}

connections += takesRelaySubscription
connections += selectedTakeRelaySubscription
return AssociatedAudio(takesRelay, selectedTakeRelay)
}

interface IDatabaseAccessors {
fun getChildren(collection: Collection): Single<List<Collection>>
fun getCollectionMetaContent(collection: Collection): Single<Content>
fun getContentByCollection(collection: Collection): Single<List<Content>>
fun updateContent(content: Content): Completable
fun getResources(content: Content, info: ResourceInfo): Observable<Content>
fun getResources(collection: Collection, info: ResourceInfo): Observable<Content>
fun getResourceInfo(content: Content): List<ResourceInfo>
fun getResourceInfo(collection: Collection): List<ResourceInfo>
fun getSubtreeResourceInfo(collection: Collection): List<ResourceInfo>
fun insertTakeForContent(take: ModelTake, content: Content): Single<Int>
fun getTakeByContent(content: Content): Single<List<Take>>
fun updateTake(take: ModelTake, date: DateHolder): Completable
}
}

private class DefaultDatabaseAccessors(
private val collectionRepo: ICollectionRepository,
private val contentRepo: IContentRepository,
private val resourceRepo: IResourceRepository,
private val takeRepo: ITakeRepository
) : WorkbookRepository.IDatabaseAccessors {
override fun getChildren(collection: Collection) = collectionRepo.getChildren(collection)

override fun getCollectionMetaContent(collection: Collection) = contentRepo.getCollectionMetaContent(collection)
override fun getContentByCollection(collection: Collection) = contentRepo.getByCollection(collection)
override fun updateContent(content: Content) = contentRepo.update(content)

override fun getResources(content: Content, info: ResourceInfo) = resourceRepo.getResources(content, info)
override fun getResources(collection: Collection, info: ResourceInfo) = resourceRepo.getResources(collection, info)
override fun getResourceInfo(content: Content) = resourceRepo.getResourceInfo(content)
override fun getResourceInfo(collection: Collection) = resourceRepo.getResourceInfo(collection)
override fun getSubtreeResourceInfo(collection: Collection) = resourceRepo.getSubtreeResourceInfo(collection)

override fun insertTakeForContent(take: ModelTake, content: Content) = takeRepo.insertForContent(take, content)
override fun getTakeByContent(content: Content) = takeRepo.getByContent(content)
override fun updateTake(take: ModelTake, date: DateHolder) = takeRepo.update(take.copy(deleted = date.value))
}
Loading

0 comments on commit 877d2fd

Please sign in to comment.