Skip to content

Commit

Permalink
fix(PnX-SI/gn_mobile_occtax#133): synchronize taxa local data accordi…
Browse files Browse the repository at this point in the history
…ng to GET -> /api/taxref/version
  • Loading branch information
sgrimault committed Sep 25, 2023
1 parent fad2c54 commit 316e5c4
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 81 deletions.
2 changes: 1 addition & 1 deletion datasync/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
id 'org.jetbrains.kotlin.android'
}

version = "0.4.5"
version = "0.4.6"

android {
namespace 'fr.geonature.datasync'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import fr.geonature.datasync.api.model.Media
import fr.geonature.datasync.api.model.NomenclatureType
import fr.geonature.datasync.api.model.Taxref
import fr.geonature.datasync.api.model.TaxrefArea
import fr.geonature.datasync.api.model.TaxrefVersion
import fr.geonature.datasync.api.model.User
import fr.geonature.datasync.auth.ICookieManager
import okhttp3.MediaType.Companion.toMediaTypeOrNull
Expand Down Expand Up @@ -174,6 +175,13 @@ class GeoNatureAPIClientImpl(private val cookieManager: ICookieManager) : IGeoNa
)
}

override fun getTaxrefVersion(): Call<TaxrefVersion> {
val taxHubService = taxHubService
?: throw MissingConfigurationException.MissingTaxHubBaseURLException

return taxHubService.getTaxrefVersion()
}

override fun getNomenclatures(): Call<List<NomenclatureType>> {
val geoNatureService = geoNatureService
?: throw MissingConfigurationException.MissingGeoNatureBaseURLException
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import fr.geonature.datasync.api.model.Media
import fr.geonature.datasync.api.model.NomenclatureType
import fr.geonature.datasync.api.model.Taxref
import fr.geonature.datasync.api.model.TaxrefArea
import fr.geonature.datasync.api.model.TaxrefVersion
import fr.geonature.datasync.api.model.User
import okhttp3.ResponseBody
import retrofit2.Call
Expand Down Expand Up @@ -108,6 +109,8 @@ interface IGeoNatureAPIClient {
offset: Int? = null,
): Call<List<TaxrefArea>>

fun getTaxrefVersion(): Call<TaxrefVersion>

fun getNomenclatures(): Call<List<NomenclatureType>>

fun getDefaultNomenclaturesValues(module: String): Call<ResponseBody>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fr.geonature.datasync.api

import fr.geonature.datasync.api.model.Taxref
import fr.geonature.datasync.api.model.TaxrefVersion
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.GET
Expand All @@ -26,4 +27,8 @@ interface ITaxHubService {
@Query("limit") limit: Int? = null,
@Query("offset") offset: Int? = null
): Call<List<Taxref>>

@Headers("Accept: application/json")
@GET("api/taxref/version")
fun getTaxrefVersion(): Call<TaxrefVersion>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package fr.geonature.datasync.api.model

import com.google.gson.annotations.SerializedName
import java.util.Date

/**
* Taxref version metadata.
*
* @author S. Grimault
*/
data class TaxrefVersion(
@SerializedName("referencial_name") val referentialName: String,
@SerializedName("version") val version: Int,
@SerializedName("update_date") val updatedAt: Date
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package fr.geonature.datasync.sync.usecase

import android.app.Application
import android.content.SharedPreferences
import android.text.TextUtils
import androidx.core.content.edit
import androidx.preference.PreferenceManager.getDefaultSharedPreferences
import androidx.room.withTransaction
import androidx.work.WorkInfo
import fr.geonature.commons.data.GeoNatureModuleName
Expand All @@ -15,6 +18,7 @@ import fr.geonature.commons.data.entity.Taxon
import fr.geonature.commons.data.entity.TaxonArea
import fr.geonature.commons.data.entity.Taxonomy
import fr.geonature.commons.interactor.BaseFlowUseCase
import fr.geonature.commons.util.toIsoDateString
import fr.geonature.datasync.R
import fr.geonature.datasync.api.IGeoNatureAPIClient
import fr.geonature.datasync.api.error.BaseApiException
Expand All @@ -34,6 +38,7 @@ import org.json.JSONObject
import org.tinylog.Logger
import retrofit2.await
import java.io.BufferedReader
import java.util.Date
import java.util.Locale
import javax.inject.Inject

Expand All @@ -50,6 +55,8 @@ class DataSyncUseCase @Inject constructor(
@SynchronizeAdditionalFieldsRepository private val synchronizeAdditionalFieldsRepository: ISynchronizeLocalDataRepository
) : BaseFlowUseCase<DataSyncStatus, DataSyncUseCase.Params>() {

private val sharedPreferences: SharedPreferences = getDefaultSharedPreferences(application)

override suspend fun run(params: Params): Flow<DataSyncStatus> =
channelFlow {
database.withTransaction {
Expand Down Expand Up @@ -607,104 +614,134 @@ class DataSyncUseCase @Inject constructor(
withAdditionalData: Boolean = true
): Flow<DataSyncStatus> =
flow {
Logger.info { "synchronize taxa..." }

runCatching {
database
.taxonDao()
.deleteAll()
}.onFailure { Logger.warn(it) { "failed to deleting existing taxa" } }
val lastUpdatedDate = getTaxaLastUpdatedDate()?.also {
Logger.info { "taxa last synchronization date: ${it.toIsoDateString()}" }
}
val taxaLastUpdatedDate = runCatching {
geoNatureAPIClient
.getTaxrefVersion()
.await()
}
.onFailure {
Logger.warn { "failed to get taxa last updated date from GeoNature..." }
}
.getOrNull()?.updatedAt?.also {
Logger.info { "taxa last synchronization date from remote: ${it.toIsoDateString()}" }
}

var hasNext: Boolean
var offset = 0
var hasErrors = false

val validTaxaIds = mutableSetOf<Long>()

// fetch all taxa from paginated list
do {
val taxrefResponse = runCatching {
geoNatureAPIClient
.getTaxref(
taxRefListId,
pageSize,
offset
)
.await()
}
.onFailure {
Logger.warn { "taxa synchronization finished with errors" }
if (lastUpdatedDate == null || taxaLastUpdatedDate == null || taxaLastUpdatedDate.after(lastUpdatedDate)) {
Logger.info { "synchronize taxa..." }

emit(
onFailure(
it,
application.getString(R.string.sync_data_taxa_with_errors)
runCatching {
database
.taxonDao()
.deleteAll()
}.onFailure { Logger.warn(it) { "failed to deleting existing taxa" } }

// fetch all taxa from paginated list
do {
val taxrefResponse = runCatching {
geoNatureAPIClient
.getTaxref(
taxRefListId,
pageSize,
offset
)
)
.await()
}
.getOrNull()
?: return@flow
.onFailure {
Logger.warn { "taxa synchronization finished with errors" }
hasErrors = true
emit(
onFailure(
it,
application.getString(R.string.sync_data_taxa_with_errors)
)
)
}
.getOrNull()
?: return@flow

if (taxrefResponse.isEmpty()) {
hasNext = false
continue
}
if (taxrefResponse.isEmpty()) {
hasNext = false
continue
}

val taxa = taxrefResponse
.asSequence()
.map { taxRef ->
// check if this taxon as a valid taxonomy definition
if (taxRef.kingdom.isNullOrBlank() || taxRef.group.isNullOrBlank()) {
Logger.warn { "invalid taxon with ID '${taxRef.id}' found: no taxonomy defined" }
val taxa = taxrefResponse
.asSequence()
.map { taxRef ->
// check if this taxon as a valid taxonomy definition
if (taxRef.kingdom.isNullOrBlank() || taxRef.group.isNullOrBlank()) {
Logger.warn { "invalid taxon with ID '${taxRef.id}' found: no taxonomy defined" }

return@map null
}

return@map null
Taxon(
id = taxRef.id,
name = taxRef.name.trim(),
taxonomy = Taxonomy(
taxRef.kingdom,
taxRef.group
),
commonName = taxRef.commonName?.trim(),
description = taxRef.fullName.trim(),
rank = ".+\\[(\\w+) - \\d+]"
.toRegex()
.find(taxRef.description)?.groupValues
?.elementAtOrNull(1)
?.let { "${it.uppercase(Locale.ROOT)} - ${taxRef.id}" },
)
}
.filterNotNull()
.onEach {
validTaxaIds.add(it.id)
}
.toList()
.toTypedArray()

Taxon(
id = taxRef.id,
name = taxRef.name.trim(),
taxonomy = Taxonomy(
taxRef.kingdom,
taxRef.group
),
commonName = taxRef.commonName?.trim(),
description = taxRef.fullName.trim(),
rank = ".+\\[(\\w+) - \\d+]"
.toRegex()
.find(taxRef.description)?.groupValues
?.elementAtOrNull(1)
?.let { "${it.uppercase(Locale.ROOT)} - ${taxRef.id}" },
)
}
.filterNotNull()
.onEach {
validTaxaIds.add(it.id)
runCatching {
database
.taxonDao()
.insert(*taxa)
}.onFailure {
Logger.warn(it) { "failed to update taxa (offset: $offset)" }
hasErrors = true
}
.toList()
.toTypedArray()

runCatching {
database
.taxonDao()
.insert(*taxa)
}.onFailure { Logger.warn(it) { "failed to update taxa (offset: $offset)" } }

Logger.info { "taxa to update: ${offset + taxa.size}" }
Logger.info { "taxa to update: ${offset + taxa.size}" }

emit(
DataSyncStatus(
state = WorkInfo.State.SUCCEEDED,
syncMessage = application.getString(
R.string.sync_data_taxa,
(offset + taxa.size)
emit(
DataSyncStatus(
state = WorkInfo.State.SUCCEEDED,
syncMessage = application.getString(
R.string.sync_data_taxa,
(offset + taxa.size)
)
)
)
)

offset += pageSize
hasNext = taxrefResponse.size == pageSize
} while (hasNext)
offset += pageSize
hasNext = taxrefResponse.size == pageSize
} while (hasNext && !hasErrors)

delay(1000)
updateTaxaLastUpdatedDate()

delay(1000)
} else {
validTaxaIds.addAll(runCatching {
database
.taxonDao()
.findAll()
.map { it.id }
}.getOrDefault(emptyList()))
}

if (withAdditionalData) {
Logger.info { "synchronize taxa additional data..." }
Expand Down Expand Up @@ -732,6 +769,9 @@ class DataSyncUseCase @Inject constructor(
)
}
.getOrNull()
?.also {
Logger.warn { "failed to fetch taxa by area from offset $offset" }
}
?: return@flow

if (taxrefAreasResponse.isEmpty()) {
Expand Down Expand Up @@ -826,6 +866,25 @@ class DataSyncUseCase @Inject constructor(
}
}

fun getTaxaLastUpdatedDate(): Date? {
return this.sharedPreferences
.getLong(
KEY_SYNC_TAXA_LAST_UPDATED_AT,
-1L
)
.takeUnless { it == -1L }
?.let { Date(it) }
}

fun updateTaxaLastUpdatedDate() {
this.sharedPreferences.edit {
putLong(
KEY_SYNC_TAXA_LAST_UPDATED_AT,
Date().time
)
}
}

data class Params(
val withAdditionalData: Boolean = true,
val withAdditionalFields: Boolean = false,
Expand All @@ -834,4 +893,8 @@ class DataSyncUseCase @Inject constructor(
val codeAreaType: String?,
val pageSize: Int = DataSyncSettings.Builder.DEFAULT_PAGE_SIZE
)

companion object {
private const val KEY_SYNC_TAXA_LAST_UPDATED_AT = "key_sync_taxa_last_updated_at"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ class DataSyncWorker @AssistedInject constructor(
) {
getInstance(context).enqueueUniquePeriodicWork(
if (withAdditionalData) DATA_SYNC_WORKER_PERIODIC else DATA_SYNC_WORKER_PERIODIC_ESSENTIAL,
ExistingPeriodicWorkPolicy.UPDATE,
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
PeriodicWorkRequestBuilder<CheckInputsToSynchronizeWorker>(repeatInterval.toJavaDuration())
.addTag(DATA_SYNC_WORKER_TAG)
.setConstraints(
Expand Down
4 changes: 2 additions & 2 deletions datasync/version.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#Tue Sep 05 18:56:54 CEST 2023
VERSION_CODE=1110
#Mon Sep 25 18:52:43 CEST 2023
VERSION_CODE=1115

0 comments on commit 316e5c4

Please sign in to comment.