asfoury/projmag

View on GitHub
app/src/main/java/com/sdp13epfl2021/projmag/database/impl/cache/OfflineCachedCandidatureDatabase.kt

Summary

Maintainability
A
0 mins
Test Coverage
A
92%
package com.sdp13epfl2021.projmag.database.impl.cache

import com.sdp13epfl2021.projmag.database.interfaces.CandidatureDatabase
import com.sdp13epfl2021.projmag.database.interfaces.ProjectId
import com.sdp13epfl2021.projmag.database.loadFromFile
import com.sdp13epfl2021.projmag.database.saveToFile
import com.sdp13epfl2021.projmag.model.Candidature
import java.io.File
import java.io.Serializable


/**
 * This is an implementation of CandidatureDatabase that keep candidatures both in persistent storage and memory.
 * If we are offline and firebase cache is empty/disabled, it will use the data stored locally.
 * If we are offline and firebase cache is enabled, it will use both the data stored locally and in the cache (priority to `db`).
 * If we are not offline, it will use the data return by `db` (up to date).
 */
class OfflineCachedCandidatureDatabase(
    private val db: CandidatureDatabase,
    private val projectsDir: File
) : CandidatureDatabase {

    private val candidatureFilename: String = "candidatures.data"
    private val candidatures: MutableMap<ProjectId, List<Candidature>> = HashMap()

    /**
     * Load candidatures from the local storage.
     */
    init {
        try {
            projectsDir.listFiles()?.let {
                it
                    .filter(File::isDirectory)
                    .mapNotNull(File::listFiles)
                    .mapNotNull { arr -> arr.find { f -> f.parentFile != null && f.isFile && f.name == candidatureFilename } }
                    .forEach { f ->
                        candidatures[f.parentFile!!.name] = loadFromFile(
                            f,
                            SerializableCandidatureListWrapper::class
                        )?.list ?: emptyList()
                    }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    /**
     * This serializable class is used to wrap a list of Candidature.
     */
    private data class SerializableCandidatureListWrapper(val list: List<Candidature>) :
        Serializable

    /**
     * Save the current candidatures for a given projectId to the local storage.
     *
     * @param projectId the id of the project.
     */
    private fun saveCandidature(projectId: ProjectId) {
        try {
            val projectDir: File = File(projectsDir, projectId)
            projectDir.mkdirs()
            saveToFile(
                File(projectDir, candidatureFilename),
                SerializableCandidatureListWrapper(candidatures[projectId]!!)
            )
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    /**
     * Get the current candidatures for a given projectId in the cache.
     *
     * @param projectId the id of the project.
     *
     * @return a list of candidatures
     */
    private fun getLocalCandidatures(projectId: ProjectId): List<Candidature> {
        return candidatures[projectId] ?: emptyList()
    }

    /**
     * Update the local cache with the remoteList merge to the local candidatures.
     * Then save the new candidatures to local storage.
     * @param projectId the id of the project.
     * @param remoteList the remote list of candidatures.
     */
    private fun merge(projectId: ProjectId, remoteList: List<Candidature>): List<Candidature> {
        val localList: List<Candidature> = getLocalCandidatures(projectId).filter { c ->
            remoteList.all { r -> r.userId != c.userId }
        }
        val totalList = localList + remoteList
        candidatures[projectId] = totalList
        saveCandidature(projectId)
        return totalList
    }

    override fun getListOfCandidatures(
        projectID: ProjectId,
        onSuccess: (List<Candidature>) -> Unit,
        onFailure: (Exception) -> Unit
    ) {
        db.getListOfCandidatures(projectID, { remoteList ->
            //If we are offline, the remoteList could be empty or incomplete (cache).
            //We only keep them if they don't collide, with userID (priority to remote => up to date)
            val totalList: List<Candidature> = merge(projectID, remoteList)
            onSuccess(totalList)
        }, {
            //If the db failed to load (for example offline mode), we return the local cached list
            onSuccess(getLocalCandidatures(projectID))
        })
    }

    override fun pushCandidature(
        projectId: ProjectId,
        userId: String,
        newState: Candidature.State,
        onSuccess: () -> Unit,
        onFailure: (Exception) -> Unit
    ) {
        val oldList: List<Candidature> = getLocalCandidatures(projectId)
        oldList.find { candidature -> candidature.userId == userId }?.let { oldCandidature ->
            val newCandidature = Candidature(
                projectId,
                userId,
                oldCandidature.profile,
                oldCandidature.cv,
                newState
            )
            candidatures[projectId] = oldList - oldCandidature + newCandidature
            saveCandidature(projectId)
        }
        db.pushCandidature(projectId, userId, newState, onSuccess, onFailure)
    }

    override fun removeCandidature(
        projectId: ProjectId,
        userId: String,
        onSuccess: () -> Unit,
        onFailure: (Exception) -> Unit
    ) {
        candidatures[projectId] =
            getLocalCandidatures(projectId).filter { candidature -> candidature.userId != userId }
        saveCandidature(projectId)
        db.removeCandidature(projectId, userId, onSuccess, onFailure)
    }

    override fun addListener(
        projectID: ProjectId,
        onChange: (ProjectId, List<Candidature>) -> Unit
    ) {
        db.addListener(projectID) { _, newCandidatures ->
            val totalList: List<Candidature> = merge(projectID, newCandidatures)
            onChange(projectID, totalList)
        }
        getListOfCandidatures(projectID, {}, {}) //preload the candidatures
    }
}