diff --git a/commons/README.md b/commons/README.md index 6b0bda8e..52031878 100644 --- a/commons/README.md +++ b/commons/README.md @@ -33,7 +33,9 @@ The authority of this content provider is `.provider`. | **\**/taxonomy/\* | String | Fetch taxonomy matching a given kingdom | | **\**/taxonomy/\*/\* | String, String | Fetch taxonomy matching a given kingdom and group | | **\**/taxa | n/a | Fetch all taxa | +| **\**/taxa/list/# | Number | Fetch all taxa matching a given list ID | | **\**/taxa/area/# | Number | Fetch all taxa matching a given area ID | +| **\**/taxa/list/#/area/# | Number, Number | Fetch all taxa matching a given list ID and area ID | | **\**/taxa/# | Number | Fetch a taxon by ID | | **\**/taxa/#/area/# | Number, Number | Fetch a taxon by ID matching a given area ID | | **\**/nomenclature_types | n/a | Fetch all nomenclature types | diff --git a/commons/build.gradle b/commons/build.gradle index 940cab9a..7a9a05fe 100644 --- a/commons/build.gradle +++ b/commons/build.gradle @@ -6,7 +6,7 @@ plugins { id 'org.jetbrains.kotlin.android' } -version = "1.3.4" +version = "1.4.0" android { namespace 'fr.geonature.commons' diff --git a/commons/src/main/java/fr/geonature/commons/data/LocalDatabase.kt b/commons/src/main/java/fr/geonature/commons/data/LocalDatabase.kt index 08ee0fda..c92c1528 100644 --- a/commons/src/main/java/fr/geonature/commons/data/LocalDatabase.kt +++ b/commons/src/main/java/fr/geonature/commons/data/LocalDatabase.kt @@ -15,6 +15,7 @@ import fr.geonature.commons.data.dao.NomenclatureTaxonomyDao import fr.geonature.commons.data.dao.NomenclatureTypeDao import fr.geonature.commons.data.dao.TaxonAreaDao import fr.geonature.commons.data.dao.TaxonDao +import fr.geonature.commons.data.dao.TaxonListDao import fr.geonature.commons.data.dao.TaxonomyDao import fr.geonature.commons.data.entity.AdditionalField import fr.geonature.commons.data.entity.AdditionalFieldDataset @@ -29,6 +30,7 @@ import fr.geonature.commons.data.entity.NomenclatureTaxonomy import fr.geonature.commons.data.entity.NomenclatureType import fr.geonature.commons.data.entity.Taxon import fr.geonature.commons.data.entity.TaxonArea +import fr.geonature.commons.data.entity.TaxonList import fr.geonature.commons.data.entity.Taxonomy /** @@ -43,6 +45,7 @@ import fr.geonature.commons.data.entity.Taxonomy Taxonomy::class, Taxon::class, TaxonArea::class, + TaxonList::class, NomenclatureType::class, Nomenclature::class, NomenclatureTaxonomy::class, @@ -53,7 +56,7 @@ import fr.geonature.commons.data.entity.Taxonomy CodeObject::class, FieldValue::class, ], - version = 20, + version = 21, exportSchema = false ) abstract class LocalDatabase : RoomDatabase() { @@ -83,6 +86,11 @@ abstract class LocalDatabase : RoomDatabase() { */ abstract fun taxonAreaDao(): TaxonAreaDao + /** + * @return The DAO for the [TaxonList.TABLE_NAME] table. + */ + abstract fun taxonListDao(): TaxonListDao + /** * @return The DAO for the [NomenclatureType.TABLE_NAME] table. */ diff --git a/commons/src/main/java/fr/geonature/commons/data/MainContentProvider.kt b/commons/src/main/java/fr/geonature/commons/data/MainContentProvider.kt index 6251b6ac..72125545 100644 --- a/commons/src/main/java/fr/geonature/commons/data/MainContentProvider.kt +++ b/commons/src/main/java/fr/geonature/commons/data/MainContentProvider.kt @@ -29,6 +29,7 @@ import fr.geonature.commons.data.entity.Taxon import fr.geonature.commons.data.entity.Taxonomy import fr.geonature.mountpoint.model.MountPoint import fr.geonature.mountpoint.util.FileUtils +import org.tinylog.Logger import java.io.File import java.io.FileNotFoundException @@ -122,10 +123,20 @@ class MainContentProvider : ContentProvider() { Taxon.TABLE_NAME, TAXA ) + addURI( + authority, + "${Taxon.TABLE_NAME}/list/#", + TAXA_LIST_ID + ) addURI( authority, "${Taxon.TABLE_NAME}/area/#", - TAXA_AREA + TAXA_AREA_ID + ) + addURI( + authority, + "${Taxon.TABLE_NAME}/list/#/area/#", + TAXA_LIST_AREA_ID ) addURI( authority, @@ -184,7 +195,7 @@ class MainContentProvider : ContentProvider() { DATASET_ID -> "$VND_TYPE_ITEM_PREFIX/$authority.${Dataset.TABLE_NAME}" INPUT_OBSERVERS, INPUT_OBSERVERS_IDS -> "$VND_TYPE_DIR_PREFIX/$authority.${InputObserver.TABLE_NAME}" INPUT_OBSERVER_ID -> "$VND_TYPE_ITEM_PREFIX/$authority.${InputObserver.TABLE_NAME}" - TAXA, TAXA_AREA -> "$VND_TYPE_DIR_PREFIX/$authority.${Taxon.TABLE_NAME}" + TAXA, TAXA_LIST_ID, TAXA_AREA_ID, TAXA_LIST_AREA_ID -> "$VND_TYPE_DIR_PREFIX/$authority.${Taxon.TABLE_NAME}" TAXON_ID, TAXON_AREA_ID -> "$VND_TYPE_ITEM_PREFIX/$authority.${Taxon.TABLE_NAME}" TAXONOMY, TAXONOMY_KINGDOM -> "$VND_TYPE_DIR_PREFIX/$authority.${Taxonomy.TABLE_NAME}" TAXONOMY_KINGDOM_GROUP -> "$VND_TYPE_ITEM_PREFIX/$authority.${Taxonomy.TABLE_NAME}" @@ -210,61 +221,67 @@ class MainContentProvider : ContentProvider() { appContext, uri ) + DATASET, DATASET_ACTIVE -> datasetQuery( appContext, uri ) + DATASET_ID -> datasetByIdQuery( appContext, uri ) + INPUT_OBSERVERS -> inputObserversQuery( appContext, selection, selectionArgs ) + INPUT_OBSERVERS_IDS -> inputObserversByIdsQuery( appContext, uri ) + INPUT_OBSERVER_ID -> inputObserverByIdQuery( appContext, uri ) + TAXONOMY, TAXONOMY_KINGDOM, TAXONOMY_KINGDOM_GROUP -> taxonomyQuery( appContext, uri ) - TAXA -> taxaQuery( + + TAXA, TAXA_LIST_ID, TAXA_AREA_ID -> taxaQuery( appContext, + uri, selection, selectionArgs, sortOrder ) + TAXON_ID -> taxonByIdQuery( appContext, uri ) - TAXA_AREA -> taxaWithAreaQuery( - appContext, - uri, - selection, - selectionArgs, - sortOrder - ) + TAXON_AREA_ID -> taxonWithAreaByIdQuery( appContext, uri ) + NOMENCLATURE_TYPES -> nomenclatureTypesQuery(appContext) NOMENCLATURE_TYPES_DEFAULT -> defaultNomenclaturesByModule( appContext, uri ) + NOMENCLATURE_ITEMS_TAXONOMY_KINGDOM, NOMENCLATURE_ITEMS_TAXONOMY_KINGDOM_GROUP -> nomenclaturesWithTaxonomyQuery( appContext, uri ) + else -> throw IllegalArgumentException("Unknown URI (query): $uri") } } @@ -301,6 +318,7 @@ class MainContentProvider : ContentProvider() { ParcelFileDescriptor.MODE_READ_ONLY ) } + INPUT_ID -> { val packageId = uri.pathSegments .drop(uri.pathSegments.indexOf("inputs") + 1) @@ -328,6 +346,7 @@ class MainContentProvider : ContentProvider() { ParcelFileDescriptor.MODE_READ_ONLY ) } + else -> throw IllegalArgumentException("Unknown URI (openFile): $uri") } } @@ -350,6 +369,7 @@ class MainContentProvider : ContentProvider() { values ) } + else -> throw IllegalArgumentException("Unknown URI (insert): $uri") } } @@ -484,6 +504,7 @@ class MainContentProvider : ContentProvider() { lastPathSegments[0], lastPathSegments[1] ) + else -> return@also } } @@ -492,12 +513,57 @@ class MainContentProvider : ContentProvider() { private fun taxaQuery( context: Context, + uri: Uri, selection: String?, selectionArgs: Array?, sortOrder: String? ): Cursor { - return getTaxonDao(context) - .QB() + val uriRegex = "/${Taxon.TABLE_NAME}(/list/\\d+)?(/area/\\d+)?".toRegex() + + val mathResult = uri.path + ?.takeIf { uriRegex.matches(it) } + ?.let { uriRegex.find(it) } + ?: run { + Logger.warn { "invalid taxa URI: '$uri', fetch all taxa..." } + + return getTaxonDao(context) + .QB() + .whereSelection( + selection, + arrayOf( + *selectionArgs + ?: emptyArray() + ) + ) + .also { + if (sortOrder.isNullOrEmpty()) { + return@also + } + + (it as TaxonDao.QB).orderBy(sortOrder) + } + .cursor() + } + + val qb = getTaxonDao(context).QB() + + mathResult.groupValues + .drop(1) + .filterNot { it.isBlank() } + .forEach { + val id = it + .substringAfterLast("/") + .toLongOrNull() + + with(it) { + when { + startsWith("/list") -> qb.withListId(id) + startsWith("/area") -> qb.withArea(id) + } + } + } + + return qb .whereSelection( selection, arrayOf( @@ -525,35 +591,6 @@ class MainContentProvider : ContentProvider() { .cursor() } - private fun taxaWithAreaQuery( - context: Context, - uri: Uri, - selection: String?, - selectionArgs: Array?, - sortOrder: String? - ): Cursor { - val filterOnArea = uri.lastPathSegment?.toLongOrNull() - - return getTaxonDao(context) - .QB() - .withArea(filterOnArea) - .whereSelection( - selection, - arrayOf( - *selectionArgs - ?: emptyArray() - ) - ) - .also { - if (sortOrder.isNullOrEmpty()) { - return@also - } - - (it as TaxonDao.QB).orderBy(sortOrder) - } - .cursor() - } - private fun taxonWithAreaByIdQuery( context: Context, uri: Uri @@ -701,8 +738,10 @@ class MainContentProvider : ContentProvider() { const val TAXONOMY_KINGDOM_GROUP = 32 const val TAXA = 40 const val TAXON_ID = 41 - const val TAXA_AREA = 42 - const val TAXON_AREA_ID = 43 + const val TAXA_LIST_ID = 42 + const val TAXA_AREA_ID = 43 + const val TAXA_LIST_AREA_ID = 44 + const val TAXON_AREA_ID = 45 const val NOMENCLATURE_TYPES = 50 const val NOMENCLATURE_TYPES_DEFAULT = 51 const val NOMENCLATURE_ITEMS_TAXONOMY_KINGDOM = 52 diff --git a/commons/src/main/java/fr/geonature/commons/data/dao/DatasetDao.kt b/commons/src/main/java/fr/geonature/commons/data/dao/DatasetDao.kt index 0b247cc3..22eea5a8 100644 --- a/commons/src/main/java/fr/geonature/commons/data/dao/DatasetDao.kt +++ b/commons/src/main/java/fr/geonature/commons/data/dao/DatasetDao.kt @@ -1,6 +1,7 @@ package fr.geonature.commons.data.dao import androidx.room.Dao +import androidx.room.Query import fr.geonature.commons.data.entity.Dataset import fr.geonature.commons.data.helper.EntityHelper.column import fr.geonature.commons.data.helper.SQLiteSelectQueryBuilder.OrderingTerm.ASC @@ -13,6 +14,14 @@ import fr.geonature.commons.data.helper.SQLiteSelectQueryBuilder.OrderingTerm.AS @Dao abstract class DatasetDao : BaseDao() { + @Query( + """SELECT d.* + FROM ${Dataset.TABLE_NAME} d + WHERE d.${Dataset.COLUMN_ID} = :datasetId + """ + ) + abstract suspend fun findById(datasetId: Long): Dataset? + /** * Internal query builder for [Dataset]. */ diff --git a/commons/src/main/java/fr/geonature/commons/data/dao/TaxonDao.kt b/commons/src/main/java/fr/geonature/commons/data/dao/TaxonDao.kt index f5315b74..9f499276 100644 --- a/commons/src/main/java/fr/geonature/commons/data/dao/TaxonDao.kt +++ b/commons/src/main/java/fr/geonature/commons/data/dao/TaxonDao.kt @@ -5,7 +5,9 @@ import androidx.room.Query import fr.geonature.commons.data.entity.AbstractTaxon import fr.geonature.commons.data.entity.Taxon import fr.geonature.commons.data.entity.TaxonArea +import fr.geonature.commons.data.entity.TaxonList import fr.geonature.commons.data.helper.EntityHelper.column +import fr.geonature.commons.data.helper.SQLiteSelectQueryBuilder /** * Data access object for [Taxon]. @@ -57,6 +59,37 @@ abstract class TaxonDao : BaseDao() { selectQueryBuilder.columns(*Taxon.defaultProjection()) } + fun withListId(listId: Long?): QB { + if (listId == null) return this + + selectQueryBuilder + .columns(*TaxonList.defaultProjection()) + .join( + SQLiteSelectQueryBuilder.JoinOperator.DEFAULT, + TaxonList.TABLE_NAME, + "${ + column( + TaxonList.COLUMN_TAXON_ID, + TaxonList.TABLE_NAME + ).second + } = ${ + column( + AbstractTaxon.COLUMN_ID, + entityTableName + ).second + } AND ${ + column( + TaxonList.COLUMN_TAXA_LIST_ID, + TaxonList.TABLE_NAME + ).second + } = ?", + TaxonList.TABLE_NAME, + listId + ) + + return this + } + fun withArea(id: Long?): QB { if (id == null) return this diff --git a/commons/src/main/java/fr/geonature/commons/data/dao/TaxonListDao.kt b/commons/src/main/java/fr/geonature/commons/data/dao/TaxonListDao.kt new file mode 100644 index 00000000..8bfd990d --- /dev/null +++ b/commons/src/main/java/fr/geonature/commons/data/dao/TaxonListDao.kt @@ -0,0 +1,12 @@ +package fr.geonature.commons.data.dao + +import androidx.room.Dao +import fr.geonature.commons.data.entity.TaxonList + +/** + * Data access object for [TaxonList]. + * + * @author S. Grimault + */ +@Dao +abstract class TaxonListDao : BaseDao() \ No newline at end of file diff --git a/commons/src/main/java/fr/geonature/commons/data/entity/AbstractTaxon.kt b/commons/src/main/java/fr/geonature/commons/data/entity/AbstractTaxon.kt index 556db65f..e3afd50d 100644 --- a/commons/src/main/java/fr/geonature/commons/data/entity/AbstractTaxon.kt +++ b/commons/src/main/java/fr/geonature/commons/data/entity/AbstractTaxon.kt @@ -45,26 +45,18 @@ abstract class AbstractTaxon : Parcelable { @ColumnInfo(name = COLUMN_DESCRIPTION) var description: String? - /** - * The rank description of the taxon. - */ - @ColumnInfo(name = COLUMN_RANK) - var rank: String? - constructor( id: Long, name: String, taxonomy: Taxonomy, commonName: String? = null, - description: String? = null, - rank: String? = null + description: String? = null ) { this.id = id this.name = name this.taxonomy = taxonomy this.commonName = commonName this.description = description - this.rank = rank } constructor(source: Parcel) : this( @@ -72,7 +64,6 @@ abstract class AbstractTaxon : Parcelable { source.readString()!!, source.readParcelableCompat()!!, source.readString(), - source.readString(), source.readString() ) @@ -85,7 +76,6 @@ abstract class AbstractTaxon : Parcelable { if (taxonomy != other.taxonomy) return false if (commonName != other.commonName) return false if (description != other.description) return false - if (rank != other.rank) return false return true } @@ -98,8 +88,6 @@ abstract class AbstractTaxon : Parcelable { ?: 0) result = 31 * result + (description?.hashCode() ?: 0) - result = 31 * result + (rank?.hashCode() - ?: 0) return result } @@ -121,7 +109,6 @@ abstract class AbstractTaxon : Parcelable { ) it.writeString(commonName) it.writeString(description) - it.writeString(rank) } } @@ -135,7 +122,6 @@ abstract class AbstractTaxon : Parcelable { const val COLUMN_NAME = "name" const val COLUMN_NAME_COMMON = "name_common" const val COLUMN_DESCRIPTION = "description" - const val COLUMN_RANK = "rank" /** * Gets the default projection. @@ -158,10 +144,6 @@ abstract class AbstractTaxon : Parcelable { column( COLUMN_DESCRIPTION, tableAlias - ), - column( - COLUMN_RANK, - tableAlias ) ) } @@ -191,12 +173,11 @@ abstract class AbstractTaxon : Parcelable { * * @return this */ - fun byNameOrDescriptionOrRank(queryString: String?): Filter { + fun byNameOrDescription(queryString: String?): Filter { if (queryString.isNullOrBlank()) { return this } - val escapedQueryString = queryString.sqlEscape() val normalizedQueryString = queryString.sqlNormalize() this.wheres.add( @@ -216,17 +197,11 @@ abstract class AbstractTaxon : Parcelable { COLUMN_DESCRIPTION, tableAlias ) - } GLOB ? OR ${ - getColumnAlias( - COLUMN_RANK, - tableAlias - ) - } LIKE ?)", + } GLOB ?)", arrayOf( normalizedQueryString, normalizedQueryString, - normalizedQueryString, - "%$escapedQueryString%" + normalizedQueryString ) ) ) diff --git a/commons/src/main/java/fr/geonature/commons/data/entity/Dataset.kt b/commons/src/main/java/fr/geonature/commons/data/entity/Dataset.kt index c91f9699..28a87c20 100644 --- a/commons/src/main/java/fr/geonature/commons/data/entity/Dataset.kt +++ b/commons/src/main/java/fr/geonature/commons/data/entity/Dataset.kt @@ -29,38 +29,37 @@ data class Dataset( /** * The unique ID of this dataset. */ - @ColumnInfo(name = COLUMN_ID) - val id: Long, + @ColumnInfo(name = COLUMN_ID) val id: Long, /** * The related module of this dataset. */ - @ColumnInfo(name = COLUMN_MODULE) - val module: String, + @ColumnInfo(name = COLUMN_MODULE) val module: String, /** * The name of the dataset. */ - @ColumnInfo(name = COLUMN_NAME) - val name: String, + @ColumnInfo(name = COLUMN_NAME) val name: String, /** * The description of the dataset. */ - @ColumnInfo(name = COLUMN_DESCRIPTION) - val description: String?, + @ColumnInfo(name = COLUMN_DESCRIPTION) val description: String?, /** * Whether this dataset is active or not. */ - @ColumnInfo(name = COLUMN_ACTIVE) - val active: Boolean = false, + @ColumnInfo(name = COLUMN_ACTIVE) val active: Boolean = false, /** * The creation date of this dataset. */ - @ColumnInfo(name = COLUMN_CREATED_AT) - val createdAt: Date? + @ColumnInfo(name = COLUMN_CREATED_AT) val createdAt: Date?, + + /** + * The taxa list id of this dataset. + */ + @ColumnInfo(name = COLUMN_TAXA_LIST_ID) val taxaListId: Long? ) : Parcelable { companion object { @@ -80,6 +79,7 @@ data class Dataset( const val COLUMN_DESCRIPTION = "description" const val COLUMN_ACTIVE = "active" const val COLUMN_CREATED_AT = "created_at" + const val COLUMN_TAXA_LIST_ID = "taxa_list_id" /** * Gets the default projection. @@ -109,6 +109,10 @@ data class Dataset( column( COLUMN_CREATED_AT, tableAlias + ), + column( + COLUMN_TAXA_LIST_ID, + tableAlias ) ) } @@ -187,6 +191,12 @@ data class Dataset( COLUMN_CREATED_AT, tableAlias ) + ), + cursor.get( + getColumnAlias( + COLUMN_TAXA_LIST_ID, + tableAlias + ) ) ) } catch (e: Exception) { diff --git a/commons/src/main/java/fr/geonature/commons/data/entity/Taxon.kt b/commons/src/main/java/fr/geonature/commons/data/entity/Taxon.kt index 467f0e3b..d6ae092f 100644 --- a/commons/src/main/java/fr/geonature/commons/data/entity/Taxon.kt +++ b/commons/src/main/java/fr/geonature/commons/data/entity/Taxon.kt @@ -24,15 +24,13 @@ class Taxon : AbstractTaxon { name: String, taxonomy: Taxonomy, commonName: String? = null, - description: String? = null, - rank: String? = null + description: String? = null ) : super( id, name, taxonomy, commonName, - description, - rank + description ) private constructor(source: Parcel) : super(source) @@ -115,12 +113,6 @@ class Taxon : AbstractTaxon { COLUMN_DESCRIPTION, tableAlias ) - ), - cursor.get( - getColumnAlias( - COLUMN_RANK, - tableAlias - ) ) ) } catch (e: Exception) { diff --git a/commons/src/main/java/fr/geonature/commons/data/entity/TaxonList.kt b/commons/src/main/java/fr/geonature/commons/data/entity/TaxonList.kt new file mode 100644 index 00000000..70799c48 --- /dev/null +++ b/commons/src/main/java/fr/geonature/commons/data/entity/TaxonList.kt @@ -0,0 +1,64 @@ +package fr.geonature.commons.data.entity + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import fr.geonature.commons.data.helper.EntityHelper.column +import kotlinx.parcelize.Parcelize + +/** + * Describes a taxon linked with list. + * + * @author S. Grimault + */ +@Entity( + tableName = TaxonList.TABLE_NAME, + primaryKeys = [TaxonList.COLUMN_TAXON_ID, TaxonList.COLUMN_TAXA_LIST_ID], + foreignKeys = [ForeignKey( + entity = Taxon::class, + parentColumns = [AbstractTaxon.COLUMN_ID], + childColumns = [TaxonList.COLUMN_TAXON_ID], + onDelete = ForeignKey.CASCADE + )] +) +@Parcelize +data class TaxonList( + /** + * The foreign key taxon ID of taxon. + */ + @ColumnInfo(name = COLUMN_TAXON_ID) var taxonId: Long, + + /** + * The list id. + */ + @ColumnInfo(name = COLUMN_TAXA_LIST_ID) var taxaListId: Long, +) : Parcelable { + + companion object { + + /** + * The name of the 'taxa_list' table. + */ + const val TABLE_NAME = "taxa_list" + + const val COLUMN_TAXON_ID = "taxon_id" + const val COLUMN_TAXA_LIST_ID = "taxa_list_id" + + /** + * Gets the default projection. + */ + fun defaultProjection(tableAlias: String = TABLE_NAME): Array> { + return arrayOf( + column( + COLUMN_TAXON_ID, + tableAlias + ), + column( + COLUMN_TAXA_LIST_ID, + tableAlias + ) + ) + } + } +} diff --git a/commons/src/main/java/fr/geonature/commons/data/entity/TaxonWithArea.kt b/commons/src/main/java/fr/geonature/commons/data/entity/TaxonWithArea.kt index d40b9d80..42f40af6 100644 --- a/commons/src/main/java/fr/geonature/commons/data/entity/TaxonWithArea.kt +++ b/commons/src/main/java/fr/geonature/commons/data/entity/TaxonWithArea.kt @@ -20,15 +20,13 @@ class TaxonWithArea : AbstractTaxon { taxonomy: Taxonomy, commonName: String? = null, description: String? = null, - rank: String? = null, taxonArea: TaxonArea? ) : super( id, name, taxonomy, commonName, - description, - rank + description ) { this.taxonArea = taxonArea } @@ -38,8 +36,7 @@ class TaxonWithArea : AbstractTaxon { taxon.name, taxon.taxonomy, taxon.commonName, - taxon.description, - taxon.rank + taxon.description ) private constructor(source: Parcel) : super(source) { @@ -51,9 +48,7 @@ class TaxonWithArea : AbstractTaxon { if (other !is TaxonWithArea) return false if (!super.equals(other)) return false - if (taxonArea != other.taxonArea) return false - - return true + return taxonArea == other.taxonArea } override fun hashCode(): Int { diff --git a/commons/src/main/java/fr/geonature/commons/data/helper/ProviderHelper.kt b/commons/src/main/java/fr/geonature/commons/data/helper/ProviderHelper.kt index 5719a684..00a3b558 100644 --- a/commons/src/main/java/fr/geonature/commons/data/helper/ProviderHelper.kt +++ b/commons/src/main/java/fr/geonature/commons/data/helper/ProviderHelper.kt @@ -18,14 +18,20 @@ object ProviderHelper { resource: String, vararg path: String ): Uri { - val baseUri = Uri.parse("content://$authority/$resource") return if (path.isEmpty()) baseUri - else withAppendedPath(baseUri, - path - .asSequence() - .filter { it.isNotBlank() } - .joinToString("/")) + else path + .asSequence() + .filter { it.isNotBlank() } + .joinToString("/") + .takeIf { it.isNotBlank() } + ?.let { + withAppendedPath( + baseUri, + it + ) + } + ?: baseUri } } diff --git a/commons/src/main/java/fr/geonature/commons/features/dataset/DatasetModule.kt b/commons/src/main/java/fr/geonature/commons/features/dataset/DatasetModule.kt new file mode 100644 index 00000000..39ba2e68 --- /dev/null +++ b/commons/src/main/java/fr/geonature/commons/features/dataset/DatasetModule.kt @@ -0,0 +1,34 @@ +package fr.geonature.commons.features.dataset + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import fr.geonature.commons.data.dao.DatasetDao +import fr.geonature.commons.features.dataset.data.DatasetLocalDataSourceImpl +import fr.geonature.commons.features.dataset.data.IDatasetLocalDataSource +import fr.geonature.commons.features.dataset.repository.DatasetRepositoryImpl +import fr.geonature.commons.features.dataset.repository.IDatasetRepository +import javax.inject.Singleton + +/** + * Dataset module. + * + * @author S. Grimault + */ +@Module +@InstallIn(SingletonComponent::class) +object DatasetModule { + + @Singleton + @Provides + fun provideDatasetLocalDataSource(datasetDao: DatasetDao): IDatasetLocalDataSource { + return DatasetLocalDataSourceImpl(datasetDao) + } + + @Singleton + @Provides + fun provideDatasetRepository(datasetLocalDataSource: IDatasetLocalDataSource): IDatasetRepository { + return DatasetRepositoryImpl(datasetLocalDataSource) + } +} \ No newline at end of file diff --git a/commons/src/main/java/fr/geonature/commons/features/dataset/data/DatasetLocalDataSourceImpl.kt b/commons/src/main/java/fr/geonature/commons/features/dataset/data/DatasetLocalDataSourceImpl.kt new file mode 100644 index 00000000..00f268e0 --- /dev/null +++ b/commons/src/main/java/fr/geonature/commons/features/dataset/data/DatasetLocalDataSourceImpl.kt @@ -0,0 +1,17 @@ +package fr.geonature.commons.features.dataset.data + +import fr.geonature.commons.data.dao.DatasetDao +import fr.geonature.commons.data.entity.Dataset +import fr.geonature.commons.features.dataset.error.DatasetException + +/** + * Default implementation of [IDatasetLocalDataSource] using local database. + * + * @author S. Grimault + */ +class DatasetLocalDataSourceImpl(private val datasetDao: DatasetDao) : IDatasetLocalDataSource { + override suspend fun findDatasetById(datasetId: Long): Dataset { + return datasetDao.findById(datasetId) + ?: throw DatasetException.NoDatasetFoundException(datasetId) + } +} \ No newline at end of file diff --git a/commons/src/main/java/fr/geonature/commons/features/dataset/data/IDatasetLocalDataSource.kt b/commons/src/main/java/fr/geonature/commons/features/dataset/data/IDatasetLocalDataSource.kt new file mode 100644 index 00000000..ecd0d6cc --- /dev/null +++ b/commons/src/main/java/fr/geonature/commons/features/dataset/data/IDatasetLocalDataSource.kt @@ -0,0 +1,22 @@ +package fr.geonature.commons.features.dataset.data + +import fr.geonature.commons.data.entity.Dataset +import fr.geonature.commons.features.dataset.error.DatasetException + +/** + * [Dataset] local data source. + * + * @author S. Grimault + */ +interface IDatasetLocalDataSource { + + /** + * Finds [Dataset] matching given taxon ID. + * + * @param datasetId the [Dataset] identifier to find + * + * @return a [Dataset] found from given ID + * @throws DatasetException.NoDatasetFoundException if not found + */ + suspend fun findDatasetById(datasetId: Long): Dataset +} \ No newline at end of file diff --git a/commons/src/main/java/fr/geonature/commons/features/dataset/error/DatasetException.kt b/commons/src/main/java/fr/geonature/commons/features/dataset/error/DatasetException.kt new file mode 100644 index 00000000..a003670c --- /dev/null +++ b/commons/src/main/java/fr/geonature/commons/features/dataset/error/DatasetException.kt @@ -0,0 +1,22 @@ +package fr.geonature.commons.features.dataset.error + +import fr.geonature.commons.data.entity.Dataset + +/** + * Base exception about [Dataset]. + * + * @author S. Grimault + */ +sealed class DatasetException( + message: String? = null, + cause: Throwable? = null +) : RuntimeException( + message, + cause +) { + /** + * Thrown if no [Dataset] was found locally from a given ID. + */ + data class NoDatasetFoundException(val id: Long) : + DatasetException("no dataset found with ID $id") +} \ No newline at end of file diff --git a/commons/src/main/java/fr/geonature/commons/features/dataset/repository/DatasetRepositoryImpl.kt b/commons/src/main/java/fr/geonature/commons/features/dataset/repository/DatasetRepositoryImpl.kt new file mode 100644 index 00000000..351e1517 --- /dev/null +++ b/commons/src/main/java/fr/geonature/commons/features/dataset/repository/DatasetRepositoryImpl.kt @@ -0,0 +1,19 @@ +package fr.geonature.commons.features.dataset.repository + +import fr.geonature.commons.data.entity.Dataset +import fr.geonature.commons.features.dataset.data.IDatasetLocalDataSource + +/** + * Default implementation of [IDatasetRepository]. + * + * @author S. Grimault + */ +class DatasetRepositoryImpl(private val datasetLocalDataSource: IDatasetLocalDataSource) : + IDatasetRepository { + + override suspend fun getDatasetById(datasetId: Long): Result { + return runCatching { + datasetLocalDataSource.findDatasetById(datasetId) + } + } +} \ No newline at end of file diff --git a/commons/src/main/java/fr/geonature/commons/features/dataset/repository/IDatasetRepository.kt b/commons/src/main/java/fr/geonature/commons/features/dataset/repository/IDatasetRepository.kt new file mode 100644 index 00000000..09d2a6c4 --- /dev/null +++ b/commons/src/main/java/fr/geonature/commons/features/dataset/repository/IDatasetRepository.kt @@ -0,0 +1,20 @@ +package fr.geonature.commons.features.dataset.repository + +import fr.geonature.commons.data.entity.Dataset + +/** + * [Dataset] repository. + * + * @author S. Grimault + */ +interface IDatasetRepository { + + /** + * Gets [Dataset] matching given taxon ID. + * + * @param datasetId the [Dataset] identifier to find + * + * @return a [Dataset] found from given ID or [Result.Failure] if something goes wrong + */ + suspend fun getDatasetById(datasetId: Long): Result +} \ No newline at end of file diff --git a/commons/src/main/java/fr/geonature/commons/util/JsonHelper.kt b/commons/src/main/java/fr/geonature/commons/util/JsonHelper.kt index 8a11361a..ec009bca 100644 --- a/commons/src/main/java/fr/geonature/commons/util/JsonHelper.kt +++ b/commons/src/main/java/fr/geonature/commons/util/JsonHelper.kt @@ -2,6 +2,7 @@ package fr.geonature.commons.util import android.util.JsonReader import android.util.JsonToken.BOOLEAN +import android.util.JsonToken.NUMBER import android.util.JsonToken.STRING import org.json.JSONArray import org.json.JSONObject @@ -34,6 +35,22 @@ fun JSONObject.toMap(): Map = } } +/** + * Returns the long value of the next token and consuming it. + * If the next token is not a long value returns `null`. + */ +fun JsonReader.nextLongOrNull(): Long? { + return when (peek()) { + NUMBER -> { + nextLong() + } + else -> { + skipValue() + null + } + } +} + /** * Returns the string value of the next token and consuming it. * If the next token is not a string returns `null`. diff --git a/commons/src/test/java/fr/geonature/commons/data/dao/AdditionalFieldDaoTest.kt b/commons/src/test/java/fr/geonature/commons/data/dao/AdditionalFieldDaoTest.kt index 34c6e254..0a367038 100644 --- a/commons/src/test/java/fr/geonature/commons/data/dao/AdditionalFieldDaoTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/dao/AdditionalFieldDaoTest.kt @@ -229,6 +229,7 @@ internal class AdditionalFieldDaoTest { description = "Observations aléatoires de la faune, de la flore ou de la fonge", active = true, createdAt = Date.from(Instant.parse("2016-10-28T08:15:00Z")) + ,100 ), Dataset( id = 17, @@ -236,7 +237,8 @@ internal class AdditionalFieldDaoTest { name = "Jeu de données personnel de Auger Ariane", description = "Jeu de données personnel de Auger Ariane", active = true, - createdAt = Date.from(Instant.parse("2020-03-28T10:00:00Z")) + createdAt = Date.from(Instant.parse("2020-03-28T10:00:00Z")), + 100 ), Dataset( id = 30, @@ -244,7 +246,8 @@ internal class AdditionalFieldDaoTest { name = "Observation opportuniste aléatoire tout règne confondu", description = "Observation opportuniste aléatoire tout règne confondu", active = true, - createdAt = Date.from(Instant.parse("2022-11-19T12:00:00Z")) + createdAt = Date.from(Instant.parse("2022-11-19T12:00:00Z")), + 100 ) ).also { datasetDao.insert(*it.toTypedArray()) diff --git a/commons/src/test/java/fr/geonature/commons/data/dao/TaxonDaoTest.kt b/commons/src/test/java/fr/geonature/commons/data/dao/TaxonDaoTest.kt index a09ed430..92055905 100644 --- a/commons/src/test/java/fr/geonature/commons/data/dao/TaxonDaoTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/dao/TaxonDaoTest.kt @@ -212,8 +212,7 @@ class TaxonDaoTest { group = "Amphibiens" ), null, - "Salamandra atra atra (Laurenti, 1768)", - "ES - 84" + "Salamandra atra atra (Laurenti, 1768)" ), Taxon( 324L, @@ -223,8 +222,7 @@ class TaxonDaoTest { group = "Amphibiens" ), "Grenouille rousse (La)", - "Rana temporaria Linnaeus, 1758", - "ES - 324" + "Rana temporaria Linnaeus, 1758" ) ) diff --git a/commons/src/test/java/fr/geonature/commons/data/entity/DatasetTest.kt b/commons/src/test/java/fr/geonature/commons/data/entity/DatasetTest.kt index cedc7866..58a318c1 100644 --- a/commons/src/test/java/fr/geonature/commons/data/entity/DatasetTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/entity/DatasetTest.kt @@ -48,7 +48,8 @@ class DatasetTest { "Dataset #1", "description", true, - now + now, + 100 ), Dataset( 1234, @@ -56,7 +57,8 @@ class DatasetTest { "Dataset #1", "description", true, - now + now, + 100 ) ) } @@ -74,6 +76,7 @@ class DatasetTest { every { cursor.getString(3) } returns "description" every { cursor.getInt(4) } returns 1 every { cursor.getLong(5) } returns 1477642500000 + every { cursor.getLong(6) } returns 100 // when getting a dataset instance from Cursor val dataset = fromCursor(cursor) @@ -87,7 +90,8 @@ class DatasetTest { "Dataset #1", "description", true, - Date.from(Instant.parse("2016-10-28T08:15:00Z")) + Date.from(Instant.parse("2016-10-28T08:15:00Z")), + 100 ), dataset ) @@ -114,7 +118,8 @@ class DatasetTest { "Dataset #1", "description", true, - Date.from(Instant.now()) + Date.from(Instant.now()), + 100 ) // when we obtain a Parcel object to write the dataset instance to it @@ -161,6 +166,10 @@ class DatasetTest { Pair( "${Dataset.TABLE_NAME}.\"${Dataset.COLUMN_CREATED_AT}\"", "${Dataset.TABLE_NAME}_${Dataset.COLUMN_CREATED_AT}" + ), + Pair( + "${Dataset.TABLE_NAME}.\"${Dataset.COLUMN_TAXA_LIST_ID}\"", + "${Dataset.TABLE_NAME}_${Dataset.COLUMN_TAXA_LIST_ID}" ) ), defaultProjection() diff --git a/commons/src/test/java/fr/geonature/commons/data/entity/TaxonTest.kt b/commons/src/test/java/fr/geonature/commons/data/entity/TaxonTest.kt index cd67896c..21f70fff 100644 --- a/commons/src/test/java/fr/geonature/commons/data/entity/TaxonTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/entity/TaxonTest.kt @@ -71,8 +71,7 @@ class TaxonTest { "Ascidies" ), "taxon_01_common", - "desc", - "ES - 1234" + "desc" ), Taxon( 1234, @@ -82,8 +81,7 @@ class TaxonTest { "Ascidies" ), "taxon_01_common", - "desc", - "ES - 1234" + "desc" ) ) @@ -136,8 +134,7 @@ class TaxonTest { "Ascidies" ), "taxon_01_common", - "desc", - "ES - 1234" + "desc" ), taxon ) @@ -156,10 +153,6 @@ class TaxonTest { column( AbstractTaxon.COLUMN_DESCRIPTION, Taxon.TABLE_NAME - ), - column( - AbstractTaxon.COLUMN_RANK, - Taxon.TABLE_NAME ) ) -> { every { cursor.getColumnIndexOrThrow(c.second) } returns -1 @@ -247,8 +240,7 @@ class TaxonTest { "Ascidies" ), "taxon_01_common", - "desc", - "ES - 1234" + "desc" ) // when we obtain a Parcel object to write the Taxon instance to it @@ -295,10 +287,6 @@ class TaxonTest { Pair( "${Taxon.TABLE_NAME}.\"${AbstractTaxon.COLUMN_DESCRIPTION}\"", "${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_DESCRIPTION}" - ), - Pair( - "${Taxon.TABLE_NAME}.\"${AbstractTaxon.COLUMN_RANK}\"", - "${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_RANK}" ) ), defaultProjection() @@ -306,54 +294,52 @@ class TaxonTest { } @Test - fun `should build filter by name or description or rank from simple query string`() { + fun `should build filter by name or description from simple query string`() { val taxonFilterByNameAndTaxonomy = Taxon .Filter() - .byNameOrDescriptionOrRank("frelon d'") + .byNameOrDescription("frelon d'") .build() assertEquals( - "(${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME_COMMON} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_DESCRIPTION} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_RANK} LIKE ?)", + "(${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME_COMMON} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_DESCRIPTION} GLOB ?)", taxonFilterByNameAndTaxonomy.first ) assertArrayEquals( arrayOf( "*[fF][rR][eéèëêẽEÉÈËÊẼ][lL][oóòöôõõOÓÒÖÔÕ][nñNÑ] [dD]['']*", "*[fF][rR][eéèëêẽEÉÈËÊẼ][lL][oóòöôõõOÓÒÖÔÕ][nñNÑ] [dD]['']*", - "*[fF][rR][eéèëêẽEÉÈËÊẼ][lL][oóòöôõõOÓÒÖÔÕ][nñNÑ] [dD]['']*", - "%frelon d''%" + "*[fF][rR][eéèëêẽEÉÈËÊẼ][lL][oóòöôõõOÓÒÖÔÕ][nñNÑ] [dD]['']*" ), taxonFilterByNameAndTaxonomy.second ) } @Test - fun `should build filter by name or description or rank from normalized query string`() { + fun `should build filter by name or description from normalized query string`() { val taxonFilterByNameAndTaxonomy = Taxon .Filter() - .byNameOrDescriptionOrRank("âne") + .byNameOrDescription("âne") .build() assertEquals( - "(${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME_COMMON} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_DESCRIPTION} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_RANK} LIKE ?)", + "(${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME_COMMON} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_DESCRIPTION} GLOB ?)", taxonFilterByNameAndTaxonomy.first ) assertArrayEquals( arrayOf( "*[aáàäâãAÁÀÄÂÃ][nñNÑ][eéèëêẽEÉÈËÊẼ]*", "*[aáàäâãAÁÀÄÂÃ][nñNÑ][eéèëêẽEÉÈËÊẼ]*", - "*[aáàäâãAÁÀÄÂÃ][nñNÑ][eéèëêẽEÉÈËÊẼ]*", - "%âne%" + "*[aáàäâãAÁÀÄÂÃ][nñNÑ][eéèëêẽEÉÈËÊẼ]*" ), taxonFilterByNameAndTaxonomy.second ) } @Test - fun `should build filter by name or description or rank from simple query string with full taxonomy`() { + fun `should build filter by name or description from simple query string with full taxonomy`() { val taxonFilterByNameAndTaxonomy = Taxon .Filter() - .byNameOrDescriptionOrRank("as") + .byNameOrDescription("as") .byTaxonomy( Taxonomy( "Animalia", @@ -363,7 +349,7 @@ class TaxonTest { .build() assertEquals( - "(${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME_COMMON} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_DESCRIPTION} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_RANK} LIKE ?) AND ((${Taxon.TABLE_NAME}_${Taxonomy.COLUMN_KINGDOM} = ?) AND (${Taxon.TABLE_NAME}_${Taxonomy.COLUMN_GROUP} = ?))", + "(${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME_COMMON} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_DESCRIPTION} GLOB ?) AND ((${Taxon.TABLE_NAME}_${Taxonomy.COLUMN_KINGDOM} = ?) AND (${Taxon.TABLE_NAME}_${Taxonomy.COLUMN_GROUP} = ?))", taxonFilterByNameAndTaxonomy.first ) assertArrayEquals( @@ -371,7 +357,6 @@ class TaxonTest { "*[aáàäâãAÁÀÄÂÃ][sS]*", "*[aáàäâãAÁÀÄÂÃ][sS]*", "*[aáàäâãAÁÀÄÂÃ][sS]*", - "%as%", "Animalia", "Ascidies" ), @@ -380,10 +365,10 @@ class TaxonTest { } @Test - fun `should build filter by name or description or rank from simple query string with taxonomy kingdom`() { + fun `should build filter by name or description from simple query string with taxonomy kingdom`() { val taxonFilterByNameAndKingdom = Taxon .Filter() - .byNameOrDescriptionOrRank("as") + .byNameOrDescription("as") .byTaxonomy( Taxonomy( "Animalia" @@ -392,7 +377,7 @@ class TaxonTest { .build() assertEquals( - "(${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME_COMMON} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_DESCRIPTION} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_RANK} LIKE ?) AND (${Taxon.TABLE_NAME}_${Taxonomy.COLUMN_KINGDOM} = ?)", + "(${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME_COMMON} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_DESCRIPTION} GLOB ?) AND (${Taxon.TABLE_NAME}_${Taxonomy.COLUMN_KINGDOM} = ?)", taxonFilterByNameAndKingdom.first ) assertArrayEquals( @@ -400,7 +385,6 @@ class TaxonTest { "*[aáàäâãAÁÀÄÂÃ][sS]*", "*[aáàäâãAÁÀÄÂÃ][sS]*", "*[aáàäâãAÁÀÄÂÃ][sS]*", - "%as%", "Animalia" ), taxonFilterByNameAndKingdom.second diff --git a/commons/src/test/java/fr/geonature/commons/data/entity/TaxonWithAreaTest.kt b/commons/src/test/java/fr/geonature/commons/data/entity/TaxonWithAreaTest.kt index c986ed87..913d1421 100644 --- a/commons/src/test/java/fr/geonature/commons/data/entity/TaxonWithAreaTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/entity/TaxonWithAreaTest.kt @@ -51,7 +51,6 @@ class TaxonWithAreaTest { ), null, "desc", - null, null ), TaxonWithArea( @@ -63,7 +62,6 @@ class TaxonWithAreaTest { ), null, "desc", - null, null ) ) @@ -78,7 +76,6 @@ class TaxonWithAreaTest { ), "taxon_01_common", "desc", - "ES - 1234", TaxonArea( 1234, 10, @@ -96,7 +93,6 @@ class TaxonWithAreaTest { ), "taxon_01_common", "desc", - "ES - 1234", TaxonArea( 1234, 10, @@ -148,12 +144,11 @@ class TaxonWithAreaTest { every { cursor.getString(3) } returns "Ascidies" every { cursor.getString(4) } returns "taxon_01_common" every { cursor.getString(5) } returns "desc" - every { cursor.getString(6) } returns "ES - 1234" - every { cursor.getLong(7) } returns 1234 - every { cursor.getLong(8) } returns 10 - every { cursor.getString(9) } returns "red" - every { cursor.getInt(10) } returns 3 - every { cursor.getLong(11) } returns 1477642500000 + every { cursor.getLong(6) } returns 1234 + every { cursor.getLong(7) } returns 10 + every { cursor.getString(8) } returns "red" + every { cursor.getInt(9) } returns 3 + every { cursor.getLong(10) } returns 1477642500000 // when getting a TaxonWithArea instance from Cursor val taxonWithArea = fromCursor(cursor) @@ -170,7 +165,6 @@ class TaxonWithAreaTest { ), "taxon_01_common", "desc", - "ES - 1234", TaxonArea( 1234, 10, @@ -204,7 +198,6 @@ class TaxonWithAreaTest { every { cursor.getString(3) } returns "Ascidies" every { cursor.getString(4) } returns null every { cursor.getString(5) } returns "desc" - every { cursor.getString(6) } returns null // when getting a TaxonWithArea instance from Cursor val taxonWithArea = fromCursor(cursor) @@ -221,7 +214,6 @@ class TaxonWithAreaTest { ), null, "desc", - null, null ), taxonWithArea @@ -249,7 +241,6 @@ class TaxonWithAreaTest { every { cursor.getString(3) } returns "Ascidies" every { cursor.getString(4) } returns null every { cursor.getString(5) } returns "desc" - every { cursor.getString(6) } returns null every { cursor.getLong(7) } returns 0 every { cursor.getLong(8) } returns 0 @@ -268,7 +259,6 @@ class TaxonWithAreaTest { ), null, "desc", - null, null ), taxonWithArea @@ -314,7 +304,6 @@ class TaxonWithAreaTest { ), "taxon_01_common", "desc", - "ES - 1234", TaxonArea( 1234, 10, @@ -369,10 +358,6 @@ class TaxonWithAreaTest { "${Taxon.TABLE_NAME}.\"${AbstractTaxon.COLUMN_DESCRIPTION}\"", "${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_DESCRIPTION}" ), - Pair( - "${Taxon.TABLE_NAME}.\"${AbstractTaxon.COLUMN_RANK}\"", - "${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_RANK}" - ), Pair( "${TaxonArea.TABLE_NAME}.\"${TaxonArea.COLUMN_TAXON_ID}\"", "${TaxonArea.TABLE_NAME}_${TaxonArea.COLUMN_TAXON_ID}" @@ -399,10 +384,10 @@ class TaxonWithAreaTest { } @Test - fun `should build filter by name or description or rank from simple query string with area colors`() { + fun `should build filter by name or description from simple query string with area colors`() { val taxonFilterByNameAndAreaColors = (TaxonWithArea .Filter() - .byNameOrDescriptionOrRank("as") as TaxonWithArea.Filter) + .byNameOrDescription("as") as TaxonWithArea.Filter) .byAreaColors( "red", "grey'" @@ -410,15 +395,14 @@ class TaxonWithAreaTest { .build() assertEquals( - "(${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME_COMMON} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_DESCRIPTION} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_RANK} LIKE ?) AND (${TaxonArea.TABLE_NAME}_${TaxonArea.COLUMN_COLOR} IN ('red', 'grey'''))", + "(${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_NAME_COMMON} GLOB ? OR ${Taxon.TABLE_NAME}_${AbstractTaxon.COLUMN_DESCRIPTION} GLOB ?) AND (${TaxonArea.TABLE_NAME}_${TaxonArea.COLUMN_COLOR} IN ('red', 'grey'''))", taxonFilterByNameAndAreaColors.first ) assertArrayEquals( arrayOf( "*[aáàäâãAÁÀÄÂÃ][sS]*", "*[aáàäâãAÁÀÄÂÃ][sS]*", - "*[aáàäâãAÁÀÄÂÃ][sS]*", - "%as%" + "*[aáàäâãAÁÀÄÂÃ][sS]*" ), taxonFilterByNameAndAreaColors.second ) diff --git a/commons/src/test/java/fr/geonature/commons/data/helper/ProviderHelperTest.kt b/commons/src/test/java/fr/geonature/commons/data/helper/ProviderHelperTest.kt index 22221a77..5cf0a8a8 100644 --- a/commons/src/test/java/fr/geonature/commons/data/helper/ProviderHelperTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/helper/ProviderHelperTest.kt @@ -26,6 +26,15 @@ class ProviderHelperTest { ).toString() ) + assertEquals( + "content://$authority/taxa", + buildUri( + authority, + "taxa", + "" + ).toString() + ) + assertEquals( "content://$authority/taxa/123", buildUri( diff --git a/commons/src/test/java/fr/geonature/commons/features/dataset/data/DatasetLocalDataSourceTest.kt b/commons/src/test/java/fr/geonature/commons/features/dataset/data/DatasetLocalDataSourceTest.kt new file mode 100644 index 00000000..52c78843 --- /dev/null +++ b/commons/src/test/java/fr/geonature/commons/features/dataset/data/DatasetLocalDataSourceTest.kt @@ -0,0 +1,127 @@ +package fr.geonature.commons.features.dataset.data + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import fr.geonature.commons.CoroutineTestRule +import fr.geonature.commons.data.LocalDatabase +import fr.geonature.commons.data.dao.DatasetDao +import fr.geonature.commons.data.entity.Dataset +import fr.geonature.commons.features.dataset.error.DatasetException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException +import java.time.Instant +import java.util.Date + +/** + * Unit tests about [IDatasetLocalDataSource]. + * + * @author S. Grimault + */ +@ExperimentalCoroutinesApi +@RunWith(AndroidJUnit4::class) +class DatasetLocalDataSourceTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val coroutineTestRule = CoroutineTestRule() + + private lateinit var db: LocalDatabase + private lateinit var datasetDao: DatasetDao + private lateinit var datasetLocalDataSource: IDatasetLocalDataSource + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room + .inMemoryDatabaseBuilder( + context, + LocalDatabase::class.java + ) + .allowMainThreadQueries() + .build() + datasetDao = db.datasetDao() + + datasetLocalDataSource = DatasetLocalDataSourceImpl(datasetDao) + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + @Test + fun `should find dataset matching given ID`() = + runTest { + val expectedDataset = initializeDataset() + + val dataset = datasetLocalDataSource.findDatasetById(17L) + + assertEquals( + expectedDataset.first { it.id == 17L }, + dataset + ) + } + + @Test + fun `should throw NoDatasetFoundException if no dataset was found from given ID`() = + runTest { + initializeDataset() + + val exception = + runCatching { datasetLocalDataSource.findDatasetById(8L) }.exceptionOrNull() + + assertTrue(exception is DatasetException.NoDatasetFoundException) + assertEquals( + 8L, + (exception as DatasetException.NoDatasetFoundException).id + ) + } + + private fun initializeDataset(): List { + return listOf( + Dataset( + id = 1, + module = "occtax", + name = "Contact aléatoire tous règnes confondus", + description = "Observations aléatoires de la faune, de la flore ou de la fonge", + active = true, + createdAt = Date.from(Instant.parse("2016-10-28T08:15:00Z")), + 100 + ), + Dataset( + id = 17, + module = "occtax", + name = "Jeu de données personnel de Auger Ariane", + description = "Jeu de données personnel de Auger Ariane", + active = true, + createdAt = Date.from(Instant.parse("2020-03-28T10:00:00Z")), + 100 + ), + Dataset( + id = 30, + module = "occtax", + name = "Observation opportuniste aléatoire tout règne confondu", + description = "Observation opportuniste aléatoire tout règne confondu", + active = true, + createdAt = Date.from(Instant.parse("2022-11-19T12:00:00Z")), + 100 + ) + ).also { + datasetDao.insert(*it.toTypedArray()) + } + } +} \ No newline at end of file diff --git a/commons/src/test/java/fr/geonature/commons/features/nomenclature/data/AdditionalFieldLocalDataSourceTest.kt b/commons/src/test/java/fr/geonature/commons/features/nomenclature/data/AdditionalFieldLocalDataSourceTest.kt index 74a705ae..48e1b613 100644 --- a/commons/src/test/java/fr/geonature/commons/features/nomenclature/data/AdditionalFieldLocalDataSourceTest.kt +++ b/commons/src/test/java/fr/geonature/commons/features/nomenclature/data/AdditionalFieldLocalDataSourceTest.kt @@ -222,7 +222,8 @@ internal class AdditionalFieldLocalDataSourceTest { name = "Contact aléatoire tous règnes confondus", description = "Observations aléatoires de la faune, de la flore ou de la fonge", active = true, - createdAt = Date.from(Instant.parse("2016-10-28T08:15:00Z")) + createdAt = Date.from(Instant.parse("2016-10-28T08:15:00Z")), + 100 ), Dataset( id = 17, @@ -230,7 +231,8 @@ internal class AdditionalFieldLocalDataSourceTest { name = "Jeu de données personnel de Auger Ariane", description = "Jeu de données personnel de Auger Ariane", active = true, - createdAt = Date.from(Instant.parse("2020-03-28T10:00:00Z")) + createdAt = Date.from(Instant.parse("2020-03-28T10:00:00Z")), + 100 ), Dataset( id = 30, @@ -238,7 +240,8 @@ internal class AdditionalFieldLocalDataSourceTest { name = "Observation opportuniste aléatoire tout règne confondu", description = "Observation opportuniste aléatoire tout règne confondu", active = true, - createdAt = Date.from(Instant.parse("2022-11-19T12:00:00Z")) + createdAt = Date.from(Instant.parse("2022-11-19T12:00:00Z")), + 100 ) ).also { datasetDao.insert(*it.toTypedArray()) diff --git a/commons/src/test/java/fr/geonature/commons/features/taxon/data/TaxonLocalDataSourceTest.kt b/commons/src/test/java/fr/geonature/commons/features/taxon/data/TaxonLocalDataSourceTest.kt index f1b0af47..c66ad3b0 100644 --- a/commons/src/test/java/fr/geonature/commons/features/taxon/data/TaxonLocalDataSourceTest.kt +++ b/commons/src/test/java/fr/geonature/commons/features/taxon/data/TaxonLocalDataSourceTest.kt @@ -196,8 +196,7 @@ class TaxonLocalDataSourceTest { group = "Amphibiens" ), null, - "Salamandra atra atra (Laurenti, 1768)", - "ES - 84" + "Salamandra atra atra (Laurenti, 1768)" ), Taxon( 324L, @@ -207,8 +206,7 @@ class TaxonLocalDataSourceTest { group = "Amphibiens" ), "Grenouille rousse (La)", - "Rana temporaria Linnaeus, 1758", - "ES - 324" + "Rana temporaria Linnaeus, 1758" ) ) diff --git a/commons/version.properties b/commons/version.properties index ffdeb358..d5652928 100644 --- a/commons/version.properties +++ b/commons/version.properties @@ -1,2 +1,2 @@ -#Sun Aug 13 16:33:52 CEST 2023 -VERSION_CODE=4920 +#Tue Dec 05 19:05:17 CET 2023 +VERSION_CODE=4975 diff --git a/datasync/README.md b/datasync/README.md index c0259779..ca86bd49 100644 --- a/datasync/README.md +++ b/datasync/README.md @@ -15,10 +15,12 @@ Manage available applications registered from GeoNature and installed ones. ## GeoNature APIs -See [IGeoNatureService](./src/main/java/fr/geonature/datasync/api/IGeoNatureService.kt) and [ITaxHubService](./src/main/java/fr/geonature/datasync/api/ITaxHubService.kt) interfaces definition about GeoNature and TaxHub APIs endpoints consumed: +See [IGeoNatureService](./src/main/java/fr/geonature/datasync/api/IGeoNatureService.kt) +and [ITaxHubService](./src/main/java/fr/geonature/datasync/api/ITaxHubService.kt) interfaces +definition about GeoNature and TaxHub APIs endpoints consumed: | Route | Method | Description | -| ------------------------------------------- | ------ | ---------------------------------------------------------------- | +|---------------------------------------------|--------|------------------------------------------------------------------| | `/api/auth/login` | `POST` | Performs authentication. | | `/api/{module}/releve` | `POST` | Send observer's input to given GeoNature module (e.g. `occtax`). | | `/api/meta/datasets` | `GET` | Fetch datasets. | @@ -28,7 +30,8 @@ See [IGeoNatureService](./src/main/java/fr/geonature/datasync/api/IGeoNatureServ | `/api/{module}/defaultNomenclatures` | `GET` | Fetch default nomenclature values. | | `/api/gn_commons/t_mobile_apps` | `GET` | Fetch available applications. | | `/api/taxref/regnewithgroupe2` | `GET` | Fetch taxonomic ranks. | -| `/api/taxref/allnamebylist/{id}` | `GET` | Fetch taxa. | +| `/api/taxref` | `GET` | Fetch taxa. | +| `/api/taxref/version` | `GET` | Fetch the current version of the taxa database. | ## Settings @@ -65,7 +68,7 @@ or ### Parameters description | Parameter | UI | Description | Default value | -| --------------------------------- | ------- | ---------------------------------------------------- |---------------| +|-----------------------------------|---------|------------------------------------------------------|---------------| | `geonature_url` | ☑ | GeoNature URL | | | `taxhub_url` | ☐ | TaxHub URL | | | `gn_application_id` | ☐ | GeoNature application ID in UsersHub | | diff --git a/datasync/build.gradle b/datasync/build.gradle index 3351d7a8..3c8d147f 100644 --- a/datasync/build.gradle +++ b/datasync/build.gradle @@ -5,7 +5,7 @@ plugins { id 'org.jetbrains.kotlin.android' } -version = "0.4.6" +version = "0.5.0" android { namespace 'fr.geonature.datasync' diff --git a/datasync/src/main/java/fr/geonature/datasync/api/GeoNatureAPIClientImpl.kt b/datasync/src/main/java/fr/geonature/datasync/api/GeoNatureAPIClientImpl.kt index 38936834..555f9a57 100644 --- a/datasync/src/main/java/fr/geonature/datasync/api/GeoNatureAPIClientImpl.kt +++ b/datasync/src/main/java/fr/geonature/datasync/api/GeoNatureAPIClientImpl.kt @@ -4,10 +4,11 @@ import android.webkit.MimeTypeMap import fr.geonature.datasync.api.error.MissingConfigurationException import fr.geonature.datasync.api.model.AuthCredentials import fr.geonature.datasync.api.model.AuthLogin +import fr.geonature.datasync.api.model.DatasetQuery 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.TaxrefListResult import fr.geonature.datasync.api.model.TaxrefVersion import fr.geonature.datasync.api.model.User import fr.geonature.datasync.auth.ICookieManager @@ -124,11 +125,11 @@ class GeoNatureAPIClientImpl(private val cookieManager: ICookieManager) : IGeoNa return geoNatureService.deleteMediaFile(mediaId) } - override fun getMetaDatasets(): Call { + override fun getMetaDatasets(query: DatasetQuery): Call { val geoNatureService = geoNatureService ?: throw MissingConfigurationException.MissingGeoNatureBaseURLException - return geoNatureService.getMetaDatasets() + return geoNatureService.getMetaDatasets(query) } override fun getUsers(menuId: Int): Call> { @@ -146,24 +147,22 @@ class GeoNatureAPIClientImpl(private val cookieManager: ICookieManager) : IGeoNa } override fun getTaxref( - listId: Int, limit: Int?, - offset: Int? - ): Call> { + page: Int? + ): Call { val taxHubService = taxHubService ?: throw MissingConfigurationException.MissingTaxHubBaseURLException return taxHubService.getTaxref( - listId, limit, - offset + page ) } override fun getTaxrefAreas( codeAreaType: String?, limit: Int?, - offset: Int? + page: Int? ): Call> { val geoNatureService = geoNatureService ?: throw MissingConfigurationException.MissingGeoNatureBaseURLException @@ -171,7 +170,7 @@ class GeoNatureAPIClientImpl(private val cookieManager: ICookieManager) : IGeoNa return geoNatureService.getTaxrefAreas( codeAreaType, limit, - offset + page ) } diff --git a/datasync/src/main/java/fr/geonature/datasync/api/IGeoNatureAPIClient.kt b/datasync/src/main/java/fr/geonature/datasync/api/IGeoNatureAPIClient.kt index c6817e86..e133f15d 100644 --- a/datasync/src/main/java/fr/geonature/datasync/api/IGeoNatureAPIClient.kt +++ b/datasync/src/main/java/fr/geonature/datasync/api/IGeoNatureAPIClient.kt @@ -4,10 +4,11 @@ import android.os.Parcel import android.os.Parcelable import fr.geonature.datasync.api.model.AuthCredentials import fr.geonature.datasync.api.model.AuthLogin +import fr.geonature.datasync.api.model.DatasetQuery 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.TaxrefListResult import fr.geonature.datasync.api.model.TaxrefVersion import fr.geonature.datasync.api.model.User import okhttp3.ResponseBody @@ -91,22 +92,21 @@ interface IGeoNatureAPIClient { fun deleteMediaFile(mediaId: Int): Call - fun getMetaDatasets(): Call + fun getMetaDatasets(query: DatasetQuery): Call fun getUsers(menuId: Int): Call> fun getTaxonomyRanks(): Call fun getTaxref( - listId: Int, limit: Int? = null, - offset: Int? = null, - ): Call> + page: Int? = null, + ): Call fun getTaxrefAreas( codeAreaType: String? = null, limit: Int? = null, - offset: Int? = null, + page: Int? = null, ): Call> fun getTaxrefVersion(): Call diff --git a/datasync/src/main/java/fr/geonature/datasync/api/IGeoNatureService.kt b/datasync/src/main/java/fr/geonature/datasync/api/IGeoNatureService.kt index 31bd3a3c..a3c112fd 100644 --- a/datasync/src/main/java/fr/geonature/datasync/api/IGeoNatureService.kt +++ b/datasync/src/main/java/fr/geonature/datasync/api/IGeoNatureService.kt @@ -2,6 +2,7 @@ package fr.geonature.datasync.api import fr.geonature.datasync.api.model.AuthCredentials import fr.geonature.datasync.api.model.AuthLogin +import fr.geonature.datasync.api.model.DatasetQuery import fr.geonature.datasync.api.model.Media import fr.geonature.datasync.api.model.NomenclatureType import fr.geonature.datasync.api.model.TaxrefArea @@ -57,9 +58,12 @@ interface IGeoNatureService { @DELETE("api/gn_commons/media/{id}") fun deleteMediaFile(@Path("id") mediaId: Int): Call - @Headers("Accept: application/json") - @GET("api/meta/datasets?fields=modules") - fun getMetaDatasets(): Call + @Headers( + "Accept: application/json", + "Content-Type: application/json;charset=UTF-8" + ) + @POST("api/meta/datasets?fields=modules") + fun getMetaDatasets(@Body query: DatasetQuery): Call @Headers("Accept: application/json") @GET("api/users/menu/{id}") @@ -72,7 +76,7 @@ interface IGeoNatureService { fun getTaxrefAreas( @Query("code_area_type") codeAreaType: String? = null, @Query("limit") limit: Int? = null, - @Query("offset") offset: Int? = null + @Query("page") page: Int? = null ): Call> @Headers("Accept: application/json") diff --git a/datasync/src/main/java/fr/geonature/datasync/api/ITaxHubService.kt b/datasync/src/main/java/fr/geonature/datasync/api/ITaxHubService.kt index 4ac87416..aa7276d2 100644 --- a/datasync/src/main/java/fr/geonature/datasync/api/ITaxHubService.kt +++ b/datasync/src/main/java/fr/geonature/datasync/api/ITaxHubService.kt @@ -1,16 +1,15 @@ package fr.geonature.datasync.api -import fr.geonature.datasync.api.model.Taxref +import fr.geonature.datasync.api.model.TaxrefListResult import fr.geonature.datasync.api.model.TaxrefVersion import okhttp3.ResponseBody import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Headers -import retrofit2.http.Path import retrofit2.http.Query /** - * TaxHub API interface definition. + * _TaxHub_ API interface definition. * * @author S. Grimault */ @@ -21,12 +20,11 @@ interface ITaxHubService { fun getTaxonomyRanks(): Call @Headers("Accept: application/json") - @GET("api/taxref/allnamebylist/{id}") + @GET("api/taxref") fun getTaxref( - @Path("id") listId: Int, @Query("limit") limit: Int? = null, - @Query("offset") offset: Int? = null - ): Call> + @Query("page") page: Int? = null + ): Call @Headers("Accept: application/json") @GET("api/taxref/version") diff --git a/datasync/src/main/java/fr/geonature/datasync/api/client.kt b/datasync/src/main/java/fr/geonature/datasync/api/client.kt index a1499a58..4308f87a 100644 --- a/datasync/src/main/java/fr/geonature/datasync/api/client.kt +++ b/datasync/src/main/java/fr/geonature/datasync/api/client.kt @@ -20,7 +20,7 @@ import java.net.UnknownHostException import java.util.concurrent.TimeUnit /** - * Helper function to create a compliant HTTP client with GeoNature APIs. + * Helper function to create a compliant HTTP client with _GeoNature_ APIs. * * @author S. Grimault */ @@ -78,33 +78,38 @@ fun createServiceClient( .addInterceptor { val request = it.request() - runCatching { it.proceed(request) }.onSuccess { response -> - if (!response.isSuccessful) { - throw when (response.code) { - 400 -> BaseApiException.BadRequestException( - response.message, - response, - ) - 401 -> BaseApiException.UnauthorizedException( - response.message, - response, - ) - 404 -> BaseApiException.NotFoundException( - response.message, - response, - ) - 500 -> BaseApiException.InternalServerException( - response.message, - response, - ) - else -> BaseApiException.ApiException( - response.code, - response.message, - response, - ) + runCatching { it.proceed(request) } + .onSuccess { response -> + if (!response.isSuccessful) { + throw when (response.code) { + 400 -> BaseApiException.BadRequestException( + response.message, + response, + ) + + 401 -> BaseApiException.UnauthorizedException( + response.message, + response, + ) + + 404 -> BaseApiException.NotFoundException( + response.message, + response, + ) + + 500 -> BaseApiException.InternalServerException( + response.message, + response, + ) + + else -> BaseApiException.ApiException( + response.code, + response.message, + response, + ) + } } } - } .getOrElse { throwable -> throw when (throwable) { is SocketException, @@ -112,6 +117,7 @@ fun createServiceClient( is UnknownHostException, is ConnectionShutdownException, -> NetworkException(throwable.message) + else -> throwable } } diff --git a/datasync/src/main/java/fr/geonature/datasync/api/model/Dataset.kt b/datasync/src/main/java/fr/geonature/datasync/api/model/Dataset.kt new file mode 100644 index 00000000..90cff561 --- /dev/null +++ b/datasync/src/main/java/fr/geonature/datasync/api/model/Dataset.kt @@ -0,0 +1,12 @@ +package fr.geonature.datasync.api.model + +import com.google.gson.annotations.SerializedName + +/** + * Dataset query. + * + * @author S. Grimault + */ +data class DatasetQuery( + @SerializedName("module_code") val code: String +) \ No newline at end of file diff --git a/datasync/src/main/java/fr/geonature/datasync/api/model/Taxref.kt b/datasync/src/main/java/fr/geonature/datasync/api/model/Taxref.kt index 84dceb60..1b98a24d 100644 --- a/datasync/src/main/java/fr/geonature/datasync/api/model/Taxref.kt +++ b/datasync/src/main/java/fr/geonature/datasync/api/model/Taxref.kt @@ -3,9 +3,21 @@ package fr.geonature.datasync.api.model import com.google.gson.annotations.SerializedName /** - * GeoNature Taxref definition. + * _GeoNature_ Taxa list result. * - * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) + * @author S. Grimault + */ +data class TaxrefListResult( + val items: List, + val total: Int, + val limit: Int, + val page: Int +) + +/** + * _GeoNature_ Taxref definition. + * + * @author S. Grimault */ data class Taxref( @SerializedName("cd_nom") @@ -23,12 +35,15 @@ data class Taxref( @SerializedName("nom_vern") val commonName: String?, - @SerializedName("search_name") + @SerializedName("nom_complet") val description: String, @SerializedName("regne") val kingdom: String?, @SerializedName("group2_inpn") - val group: String? + val group: String?, + + @SerializedName("listes") + val list: List? ) diff --git a/datasync/src/main/java/fr/geonature/datasync/sync/io/DatasetJsonReader.kt b/datasync/src/main/java/fr/geonature/datasync/sync/io/DatasetJsonReader.kt index 4730677c..3bd8de06 100644 --- a/datasync/src/main/java/fr/geonature/datasync/sync/io/DatasetJsonReader.kt +++ b/datasync/src/main/java/fr/geonature/datasync/sync/io/DatasetJsonReader.kt @@ -1,17 +1,17 @@ package fr.geonature.datasync.sync.io import android.util.JsonReader +import android.util.JsonToken import android.util.MalformedJsonException import fr.geonature.commons.data.entity.Dataset +import fr.geonature.commons.util.nextLongOrNull import org.tinylog.Logger import java.io.IOException import java.io.Reader import java.io.StringReader -import java.text.ParseException import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -import java.util.TimeZone /** * Default `JsonReader` about reading a `JSON` stream and build the corresponding [Dataset]. @@ -56,16 +56,29 @@ class DatasetJsonReader { val dataset = mutableListOf() val jsonReader = JsonReader(reader) - jsonReader.beginObject() - while (jsonReader.hasNext()) { - when (jsonReader.nextName()) { - "data" -> dataset.addAll(readDatasetAsArray(jsonReader)) - else -> jsonReader.skipValue() + when (jsonReader.peek()) { + JsonToken.BEGIN_ARRAY -> { + dataset.addAll(readDatasetAsArray(jsonReader)) } - } - jsonReader.endObject() + JsonToken.BEGIN_OBJECT -> { + jsonReader.beginObject() + + while (jsonReader.hasNext()) { + when (jsonReader.nextName()) { + "data" -> dataset.addAll(readDatasetAsArray(jsonReader)) + else -> jsonReader.skipValue() + } + } + + jsonReader.endObject() + } + + else -> { + jsonReader.skipValue() + } + } jsonReader.close() @@ -95,6 +108,7 @@ class DatasetJsonReader { var active = false var createdAt: Date? = null val modules = mutableListOf() + var taxaListId: Long? = null while (reader.hasNext()) { when (reader.nextName()) { @@ -104,6 +118,10 @@ class DatasetJsonReader { "active" -> active = reader.nextBoolean() "meta_create_date" -> createdAt = toDate(reader.nextString()) "modules" -> modules.addAll(readDatasetModules(reader)) + "id_taxa_list" -> taxaListId = reader + .nextLongOrNull() + ?.coerceAtLeast(0L) + else -> reader.skipValue() } } @@ -124,7 +142,8 @@ class DatasetJsonReader { name, description, active, - createdAt + createdAt, + taxaListId ) } .toList() @@ -156,6 +175,7 @@ class DatasetJsonReader { "module_path" -> module = reader .nextString() .lowercase(Locale.ROOT) + else -> reader.skipValue() } } @@ -168,17 +188,18 @@ class DatasetJsonReader { internal fun toDate(str: String?): Date? { if (str.isNullOrBlank()) return null - val sdf = SimpleDateFormat( - "yyyy-MM-dd HH:mm:ss", - Locale.getDefault() - ).apply { - timeZone = TimeZone.getTimeZone("UTC") - } - - return try { - sdf.parse(str) - } catch (pe: ParseException) { - null + return runCatching { + SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss", + Locale.getDefault() + ).parse(str) } + .recoverCatching { + SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss", + Locale.getDefault() + ).parse(str) + } + .getOrNull() } } diff --git a/datasync/src/main/java/fr/geonature/datasync/sync/usecase/DataSyncUseCase.kt b/datasync/src/main/java/fr/geonature/datasync/sync/usecase/DataSyncUseCase.kt index d0d231f9..56475b05 100644 --- a/datasync/src/main/java/fr/geonature/datasync/sync/usecase/DataSyncUseCase.kt +++ b/datasync/src/main/java/fr/geonature/datasync/sync/usecase/DataSyncUseCase.kt @@ -16,12 +16,14 @@ import fr.geonature.commons.data.entity.NomenclatureTaxonomy import fr.geonature.commons.data.entity.NomenclatureType import fr.geonature.commons.data.entity.Taxon import fr.geonature.commons.data.entity.TaxonArea +import fr.geonature.commons.data.entity.TaxonList 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 +import fr.geonature.datasync.api.model.DatasetQuery import fr.geonature.datasync.settings.DataSyncSettings import fr.geonature.datasync.sync.DataSyncStatus import fr.geonature.datasync.sync.ServerStatus @@ -39,7 +41,6 @@ import org.tinylog.Logger import retrofit2.await import java.io.BufferedReader import java.util.Date -import java.util.Locale import javax.inject.Inject /** @@ -91,7 +92,6 @@ class DataSyncUseCase @Inject constructor( ) } synchronizeTaxa( - params.taxRefListId, params.codeAreaType, params.pageSize, params.withAdditionalData @@ -151,7 +151,7 @@ class DataSyncUseCase @Inject constructor( val response = runCatching { geoNatureAPIClient - .getMetaDatasets() + .getMetaDatasets(DatasetQuery(code = moduleName.uppercase())) .await() } .onFailure { @@ -608,7 +608,6 @@ class DataSyncUseCase @Inject constructor( } private suspend fun synchronizeTaxa( - taxRefListId: Int, codeAreaType: String?, pageSize: Int, withAdditionalData: Boolean = true @@ -631,6 +630,7 @@ class DataSyncUseCase @Inject constructor( var hasNext: Boolean var offset = 0 + var page = 1 var hasErrors = false val validTaxaIds = mutableSetOf() @@ -646,12 +646,11 @@ class DataSyncUseCase @Inject constructor( // fetch all taxa from paginated list do { - val taxrefResponse = runCatching { + val taxrefListResult = runCatching { geoNatureAPIClient .getTaxref( - taxRefListId, pageSize, - offset + page ) .await() } @@ -668,12 +667,12 @@ class DataSyncUseCase @Inject constructor( .getOrNull() ?: return@flow - if (taxrefResponse.isEmpty()) { + if (taxrefListResult.items.isEmpty()) { hasNext = false continue } - val taxa = taxrefResponse + val taxa = taxrefListResult.items .asSequence() .map { taxRef -> // check if this taxon as a valid taxonomy definition @@ -691,12 +690,7 @@ class DataSyncUseCase @Inject constructor( 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}" }, + description = taxRef.fullName?.trim() ) } .filterNotNull() @@ -706,12 +700,30 @@ class DataSyncUseCase @Inject constructor( .toList() .toTypedArray() + val taxaList = taxrefListResult.items + .asSequence() + .filter { taxRef -> validTaxaIds.any { it == taxRef.id } } + .flatMap { taxRef -> + (taxRef.list + ?: emptyList()).map { + TaxonList( + taxRef.id, + it + ) + } + } + .toList() + .toTypedArray() + runCatching { database .taxonDao() .insert(*taxa) + database + .taxonListDao() + .insert(*taxaList) }.onFailure { - Logger.warn(it) { "failed to update taxa (offset: $offset)" } + Logger.warn(it) { "failed to update taxa (page: $page)" } hasErrors = true } @@ -728,7 +740,8 @@ class DataSyncUseCase @Inject constructor( ) offset += pageSize - hasNext = taxrefResponse.size == pageSize + page++ + hasNext = taxrefListResult.items.size == pageSize } while (hasNext && !hasErrors) updateTaxaLastUpdatedDate() @@ -747,15 +760,16 @@ class DataSyncUseCase @Inject constructor( Logger.info { "synchronize taxa additional data..." } offset = 0 + page = 1 - // fetch all taxa metadata from paginated list + // fetch all taxa areas from paginated list do { val taxrefAreasResponse = runCatching { geoNatureAPIClient .getTaxrefAreas( codeAreaType, pageSize, - offset + page ) .await() } @@ -769,9 +783,6 @@ class DataSyncUseCase @Inject constructor( ) } .getOrNull() - ?.also { - Logger.warn { "failed to fetch taxa by area from offset $offset" } - } ?: return@flow if (taxrefAreasResponse.isEmpty()) { @@ -779,7 +790,7 @@ class DataSyncUseCase @Inject constructor( continue } - Logger.info { "found ${taxrefAreasResponse.size} taxa with areas from offset $offset" } + Logger.info { "found ${taxrefAreasResponse.size} taxa with areas from page $page" } emit( DataSyncStatus( @@ -810,11 +821,12 @@ class DataSyncUseCase @Inject constructor( database .taxonAreaDao() .insert(*taxonAreas) - }.onFailure { Logger.warn(it) { "failed to update taxa with areas (offset: $offset)" } } + }.onFailure { Logger.warn(it) { "failed to update taxa with areas (page: $page)" } } - Logger.info { "updating ${taxonAreas.size} taxa with areas from offset $offset" } + Logger.info { "updating ${taxonAreas.size} taxa with areas from page $page" } offset += pageSize + page++ hasNext = taxrefAreasResponse.size == pageSize } while (hasNext) } @@ -866,7 +878,7 @@ class DataSyncUseCase @Inject constructor( } } - fun getTaxaLastUpdatedDate(): Date? { + private fun getTaxaLastUpdatedDate(): Date? { return this.sharedPreferences .getLong( KEY_SYNC_TAXA_LAST_UPDATED_AT, @@ -876,7 +888,7 @@ class DataSyncUseCase @Inject constructor( ?.let { Date(it) } } - fun updateTaxaLastUpdatedDate() { + private fun updateTaxaLastUpdatedDate() { this.sharedPreferences.edit { putLong( KEY_SYNC_TAXA_LAST_UPDATED_AT, @@ -889,7 +901,6 @@ class DataSyncUseCase @Inject constructor( val withAdditionalData: Boolean = true, val withAdditionalFields: Boolean = false, val usersMenuId: Int = 0, - val taxRefListId: Int = 0, val codeAreaType: String?, val pageSize: Int = DataSyncSettings.Builder.DEFAULT_PAGE_SIZE ) diff --git a/datasync/src/main/java/fr/geonature/datasync/sync/worker/DataSyncWorker.kt b/datasync/src/main/java/fr/geonature/datasync/sync/worker/DataSyncWorker.kt index 399bc679..c22feb5c 100644 --- a/datasync/src/main/java/fr/geonature/datasync/sync/worker/DataSyncWorker.kt +++ b/datasync/src/main/java/fr/geonature/datasync/sync/worker/DataSyncWorker.kt @@ -93,10 +93,6 @@ class DataSyncWorker @AssistedInject constructor( INPUT_USERS_MENU_ID, 0 ), - taxRefListId = inputData.getInt( - INPUT_TAXREF_LIST_ID, - 0 - ), codeAreaType = inputData.getString(INPUT_CODE_AREA_TYPE), pageSize = inputData.getInt( INPUT_PAGE_SIZE, @@ -246,7 +242,6 @@ class DataSyncWorker @AssistedInject constructor( const val AUTH_NOTIFICATION_ID = 4 private const val INPUT_USERS_MENU_ID = "usersMenuId" - private const val INPUT_TAXREF_LIST_ID = "taxrefListId" private const val INPUT_CODE_AREA_TYPE = "codeAreaType" private const val INPUT_PAGE_SIZE = "pageSize" private const val INPUT_WITH_ADDITIONAL_DATA = "withAdditionalData" @@ -355,10 +350,6 @@ class DataSyncWorker @AssistedInject constructor( INPUT_USERS_MENU_ID, dataSyncSettings.usersListId ) - .putInt( - INPUT_TAXREF_LIST_ID, - dataSyncSettings.taxrefListId - ) .putString( INPUT_CODE_AREA_TYPE, dataSyncSettings.codeAreaType diff --git a/datasync/src/main/res/values-fr/strings.xml b/datasync/src/main/res/values-fr/strings.xml index bdc66da6..07769423 100644 --- a/datasync/src/main/res/values-fr/strings.xml +++ b/datasync/src/main/res/values-fr/strings.xml @@ -45,6 +45,8 @@ Valeurs par défaut de la nomenclature : échec Taxons : %1$d Synchronisation des taxons terminée avec des erreurs + Liste des taxons: %1$d + Synchronisation des listes de taxons terminée avec des erreurs Unités géographiques : %1$d Synchronisation des unités géographiques terminée avec des erreurs Champs additionnels : %1$d diff --git a/datasync/src/main/res/values/strings.xml b/datasync/src/main/res/values/strings.xml index fa74177d..dbb13e4f 100644 --- a/datasync/src/main/res/values/strings.xml +++ b/datasync/src/main/res/values/strings.xml @@ -45,6 +45,8 @@ Nomenclature default values: failure Taxa: %1$d Taxa synchronization finished with errors + Taxa list: %1$d + Taxa list synchronization finished with errors Taxa by area: %1$d Taxa by area synchronization finished with errors Additional fields: %1$d diff --git a/datasync/src/test/java/fr/geonature/datasync/sync/io/DatasetJsonReaderTest.kt b/datasync/src/test/java/fr/geonature/datasync/sync/io/DatasetJsonReaderTest.kt index 617064a1..1b86077c 100644 --- a/datasync/src/test/java/fr/geonature/datasync/sync/io/DatasetJsonReaderTest.kt +++ b/datasync/src/test/java/fr/geonature/datasync/sync/io/DatasetJsonReaderTest.kt @@ -58,7 +58,7 @@ class DatasetJsonReaderTest { @Test fun testRead() { // given an input file to read - val json = getFixture("metadataset_geonature.json") + val json = getFixture("dataset_geonature.json") // when parsing this file val dataset = datasetJsonReader.read(json) @@ -73,7 +73,8 @@ class DatasetJsonReaderTest { "Dataset #1", "Description of Dataset #1", true, - datasetJsonReader.toDate("2019-10-30 22:32:16.591174") + datasetJsonReader.toDate("2019-10-30T22:32:16.591174"), + 100 ), Dataset( 19L, @@ -81,7 +82,8 @@ class DatasetJsonReaderTest { "Dataset #2", "Description of Dataset #2", false, - datasetJsonReader.toDate("2019-11-13 10:08:47.762240") + datasetJsonReader.toDate("2019-11-13 10:08:47.762240"), + null ), Dataset( 19L, @@ -89,7 +91,8 @@ class DatasetJsonReaderTest { "Dataset #2", "Description of Dataset #2", false, - datasetJsonReader.toDate("2019-11-13 10:08:47.762240") + datasetJsonReader.toDate("2019-11-13 10:08:47.762240"), + null ) ), dataset.toTypedArray() diff --git a/datasync/src/test/resources/fixtures/metadataset_geonature.json b/datasync/src/test/resources/fixtures/dataset_geonature.json similarity index 97% rename from datasync/src/test/resources/fixtures/metadataset_geonature.json rename to datasync/src/test/resources/fixtures/dataset_geonature.json index 6c916a0c..f6319bcc 100644 --- a/datasync/src/test/resources/fixtures/metadataset_geonature.json +++ b/datasync/src/test/resources/fixtures/dataset_geonature.json @@ -37,7 +37,7 @@ "id_cda": 37 } ], - "meta_create_date": "2019-10-30 22:32:16.591174", + "meta_create_date": "2019-10-30T22:32:16.591174", "meta_update_date": null, "bbox_west": null, "id_nomenclature_dataset_objectif": 415, @@ -60,7 +60,8 @@ "module_external_url": null, "id_module": 4 } - ] + ], + "id_taxa_list": 100 }, { "id_nomenclature_resource_type": 324, diff --git a/datasync/version.properties b/datasync/version.properties index 7fb8e9e0..b535a386 100644 --- a/datasync/version.properties +++ b/datasync/version.properties @@ -1,2 +1,2 @@ -#Mon Sep 25 18:52:43 CEST 2023 -VERSION_CODE=1115 +#Tue Dec 05 19:05:17 CET 2023 +VERSION_CODE=1160