diff --git a/core/gradle.properties b/core/gradle.properties index c731971e76..b92d881dff 100644 --- a/core/gradle.properties +++ b/core/gradle.properties @@ -29,8 +29,8 @@ # Properties which are consumed by plugins/gradle-mvn-push.gradle plugin. # They are used for publishing artifact to snapshot repository. -VERSION_NAME=1.8.1.1 -VERSION_CODE=282 +VERSION_NAME=1.8.2 +VERSION_CODE=283 GROUP=org.hisp.dhis diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/data/database/ObjectStoreAbstractIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/data/database/ObjectStoreAbstractIntegrationShould.kt index a6fee2b337..16333d0398 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/data/database/ObjectStoreAbstractIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/data/database/ObjectStoreAbstractIntegrationShould.kt @@ -34,6 +34,7 @@ import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter import org.hisp.dhis.android.core.arch.db.stores.internal.ObjectStore import org.hisp.dhis.android.core.arch.db.tableinfos.TableInfo import org.hisp.dhis.android.core.common.CoreObject +import org.junit.After import org.junit.Before import org.junit.Test @@ -54,6 +55,11 @@ abstract class ObjectStoreAbstractIntegrationShould internal con store.delete() } + @After + open fun tearDown() { + store.delete() + } + @Test fun insert_and_select_first_object() { store.insert(`object`) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/BasePayloadGeneratorMockIntegration.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/BasePayloadGeneratorMockIntegration.kt index 1b9eacb248..55aeeeab89 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/BasePayloadGeneratorMockIntegration.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/BasePayloadGeneratorMockIntegration.kt @@ -70,6 +70,7 @@ open class BasePayloadGeneratorMockIntegration : BaseMockIntegrationTestMetadata protected val event2Id = "event2Id" protected val event3Id = "event3Id" protected val singleEventId = "singleEventId" + protected val unassignedDataElementId = "bx6fsa0t90x" @After @Throws(D2Error::class) diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackerImporterPayloadGeneratorMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackerImporterPayloadGeneratorMockIntegrationShould.kt index dc3e15ffa3..efd97fdb3b 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackerImporterPayloadGeneratorMockIntegrationShould.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackerImporterPayloadGeneratorMockIntegrationShould.kt @@ -205,7 +205,7 @@ class OldTrackerImporterPayloadGeneratorMockIntegrationShould : BasePayloadGener } @Test - fun build_payload_with_non_accessible_adta() { + fun build_payload_with_non_accessible_data() { storeTrackerData() val previousTrackedEntityType = trackedEntityTypeStore.selectFirst()!! val previousProgram = programStore.selectFirst()!! @@ -228,4 +228,22 @@ class OldTrackerImporterPayloadGeneratorMockIntegrationShould : BasePayloadGener trackedEntityTypeStore.update(previousTrackedEntityType) programStore.update(previousProgram) } + + @Test + fun do_not_include_data_values_not_assigned_to_program_stage() { + storeTrackerData() + d2.trackedEntityModule().trackedEntityDataValues() + .value(event1Id, unassignedDataElementId) + .blockingSet("value") + + val instance = teiStore.selectByUid(teiId)!! + val payload = oldTrackerPayloadGenerator.getTrackedEntityInstancePayload(listOf(instance)) + + val events = getEvents(getEnrollments(payload.trackedEntityInstances.first()).first()) + val event1 = events.find { it.uid() == event1Id } + assertThat(event1).isNotNull() + + val eventValues = event1!!.trackedEntityDataValues()!! + assertThat(eventValues.any { it.dataElement() == unassignedDataElementId }).isFalse() + } } diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityDataValueStoreIntegrationShould.java b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityDataValueStoreIntegrationShould.java index 10967c7776..2b0a05b3a0 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityDataValueStoreIntegrationShould.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityDataValueStoreIntegrationShould.java @@ -30,15 +30,25 @@ import com.google.common.collect.Lists; +import org.hisp.dhis.android.core.arch.db.stores.internal.IdentifiableObjectStore; +import org.hisp.dhis.android.core.common.ObjectWithUid; import org.hisp.dhis.android.core.common.State; import org.hisp.dhis.android.core.data.database.ObjectWithoutUidStoreAbstractIntegrationShould; +import org.hisp.dhis.android.core.data.dataelement.DataElementSamples; +import org.hisp.dhis.android.core.data.program.ProgramStageDataElementSamples; +import org.hisp.dhis.android.core.data.program.ProgramStageSamples; import org.hisp.dhis.android.core.data.trackedentity.EventSamples; import org.hisp.dhis.android.core.data.trackedentity.TrackedEntityDataValueSamples; import org.hisp.dhis.android.core.enrollment.Enrollment; import org.hisp.dhis.android.core.enrollment.internal.EnrollmentStore; import org.hisp.dhis.android.core.enrollment.internal.EnrollmentStoreImpl; +import org.hisp.dhis.android.core.event.Event; import org.hisp.dhis.android.core.event.internal.EventStore; import org.hisp.dhis.android.core.event.internal.EventStoreImpl; +import org.hisp.dhis.android.core.program.ProgramStage; +import org.hisp.dhis.android.core.program.ProgramStageDataElement; +import org.hisp.dhis.android.core.program.internal.ProgramStageDataElementStore; +import org.hisp.dhis.android.core.program.internal.ProgramStageStore; import org.hisp.dhis.android.core.trackedentity.TrackedEntityDataValue; import org.hisp.dhis.android.core.trackedentity.TrackedEntityDataValueTableInfo; import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance; @@ -73,7 +83,12 @@ protected TrackedEntityDataValue buildObject() { @After public void tearDown() { + super.tearDown(); + TrackedEntityInstanceStoreImpl.create(TestDatabaseAdapterFactory.get()).delete(); + EnrollmentStoreImpl.create(TestDatabaseAdapterFactory.get()).delete(); EventStoreImpl.create(TestDatabaseAdapterFactory.get()).delete(); + ProgramStageStore.create(TestDatabaseAdapterFactory.get()).delete(); + ProgramStageDataElementStore.create(TestDatabaseAdapterFactory.get()).delete(); } @Override @@ -179,4 +194,36 @@ public void select_tracker_data_values() { assertThat(trackerDataValues.get("event_1").iterator().next().event()).isEqualTo("event_1"); assertThat(trackerDataValues.get("event_2")).isNull(); } + + @Test + public void remove_unassigned_event_values() { + ProgramStage stage = ProgramStageSamples.getProgramStage().toBuilder().uid("stage_uid").build(); + IdentifiableObjectStore stageStore = ProgramStageStore.create(TestDatabaseAdapterFactory.get()); + stageStore.insert(stage); + + Event event = EventSamples.get().toBuilder().uid("event_1").programStage(stage.uid()).build(); + EventStore eventStore = EventStoreImpl.create(TestDatabaseAdapterFactory.get()); + eventStore.insert(event); + + String dataElement1 = "data_element_1"; + String dataElement2 = "data_element_2"; + IdentifiableObjectStore psStore = + ProgramStageDataElementStore.create(TestDatabaseAdapterFactory.get()); + psStore.insert(ProgramStageDataElementSamples.getProgramStageDataElement().toBuilder() + .uid(dataElement1) + .dataElement(DataElementSamples.getDataElement().toBuilder().uid(dataElement1).build()) + .programStage(ObjectWithUid.create(stage.uid())) + .build()); + + store.insert(TrackedEntityDataValueSamples.get() + .toBuilder().event(event.uid()).dataElement(dataElement1).build()); + store.insert(TrackedEntityDataValueSamples.get() + .toBuilder().event(event.uid()).dataElement(dataElement2).build()); + assertThat(store.queryTrackedEntityDataValuesByEventUid(event.uid()).size()).isEqualTo(2); + + store.removeUnassignedDataValuesByEvent(event.uid()); + List values = store.queryTrackedEntityDataValuesByEventUid(event.uid()); + assertThat(values.size()).isEqualTo(1); + assertThat(values.get(0).dataElement()).isEqualTo(dataElement1); + } } \ No newline at end of file diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/user/internal/LogInOfflineCallMockIntegrationShould.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/user/internal/LogInOfflineCallMockIntegrationShould.kt new file mode 100644 index 0000000000..9c65e3efb9 --- /dev/null +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/user/internal/LogInOfflineCallMockIntegrationShould.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2004-2023, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.user.internal + +import com.google.common.truth.Truth.assertThat +import org.hisp.dhis.android.core.user.User +import org.hisp.dhis.android.core.utils.integration.mock.BaseMockIntegrationTestMethodScopedEmptyEnqueable +import org.hisp.dhis.android.core.utils.runner.D2JunitRunner +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(D2JunitRunner::class) +class LogInOfflineCallMockIntegrationShould : BaseMockIntegrationTestMethodScopedEmptyEnqueable() { + + @Test + fun login_offline_on_connection_error() { + dhis2MockServer.enqueueLoginResponses() + + login() + assertThat(getUser()).isNotNull() + + logout() + assertThrowsException { getUser() } + + dhis2MockServer.shutdown() + + login() + assertThat(d2.userModule().user().blockingGet()).isNotNull() + } + + private fun login(): User { + return d2.userModule().blockingLogIn("test_user", "test_password", dhis2MockServer.baseEndpoint) + } + + private fun logout() { + if (d2.userModule().blockingIsLogged()) { + d2.userModule().blockingLogOut() + } + } + + private fun getUser(): User? { + return d2.userModule().user().blockingGet() + } + + private fun assertThrowsException(block: () -> Any?) { + try { + block() + fail("Get user should fail after logout") + } catch (_: RuntimeException) { + // + } + } +} diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/BaseMockIntegrationTest.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/BaseMockIntegrationTest.kt index fd73098e90..338a7bc17a 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/BaseMockIntegrationTest.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/BaseMockIntegrationTest.kt @@ -64,5 +64,10 @@ abstract class BaseMockIntegrationTest { } return tuple.isNewInstance } + + @JvmStatic + fun removeObjects(content: MockIntegrationTestDatabaseContent) { + MockIntegrationTestObjectsFactory.removeObjects(content) + } } } diff --git a/core/src/test/java/org/hisp/dhis/android/core/arch/helpers/UserHelperShould.java b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/BaseMockIntegrationTestMethodScopedEmptyEnqueable.kt similarity index 61% rename from core/src/test/java/org/hisp/dhis/android/core/arch/helpers/UserHelperShould.java rename to core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/BaseMockIntegrationTestMethodScopedEmptyEnqueable.kt index 77ec448982..d0967a819f 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/arch/helpers/UserHelperShould.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/BaseMockIntegrationTestMethodScopedEmptyEnqueable.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022, University of Oslo + * Copyright (c) 2004-2023, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -25,38 +25,20 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.utils.integration.mock -package org.hisp.dhis.android.core.arch.helpers; +import org.junit.After +import org.junit.Before -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +abstract class BaseMockIntegrationTestMethodScopedEmptyEnqueable : BaseMockIntegrationTest() { -import static com.google.common.truth.Truth.assertThat; - -@RunWith(JUnit4.class) -public class UserHelperShould { - - @Test - public void md5_evaluate_same_string() { - String md5s1 = UserHelper.md5("user1","password1"); - String md5s2 = UserHelper.md5("user1","password1"); - - assertThat(md5s1.length()).isEqualTo(32); - assertThat(md5s2.length()).isEqualTo(32); - - assertThat(md5s1.equals(md5s2)).isTrue(); + @Before + fun setup() { + setUpClass(MockIntegrationTestDatabaseContent.MethodScopedEmptyEnqueable) } - @Test - public void md5_evaluate_different_string() { - String md5s1 = UserHelper.md5("user2", "password2"); - String md5s2 = UserHelper.md5("user3", "password3"); - - assertThat(md5s1.length()).isEqualTo(32); - assertThat(md5s2.length()).isEqualTo(32); - - assertThat(md5s1.equals(md5s2)).isFalse(); + @After + fun tearDown() { + removeObjects(MockIntegrationTestDatabaseContent.MethodScopedEmptyEnqueable) } - } diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestDatabaseContent.java b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestDatabaseContent.java index fd8d6bc03e..c2c41e1823 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestDatabaseContent.java +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestDatabaseContent.java @@ -34,6 +34,7 @@ public enum MockIntegrationTestDatabaseContent { FullDispatcher, MetadataEnqueable, MetadataDispatcher, + MethodScopedEmptyEnqueable, LocalAnalyticsDefaultDispatcher, LocalAnalyticsLargeDispatcher, LocalAnalyticsSuperLargeDispatcher diff --git a/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestObjectsFactory.kt b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestObjectsFactory.kt index 3b94b35b14..0228db6d2d 100644 --- a/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestObjectsFactory.kt +++ b/core/src/androidTest/java/org/hisp/dhis/android/core/utils/integration/mock/MockIntegrationTestObjectsFactory.kt @@ -43,6 +43,14 @@ internal object MockIntegrationTestObjectsFactory { } } + fun removeObjects(content: MockIntegrationTestDatabaseContent) { + val instance = instances[content] + if (instance != null) { + instance.tearDown() + instances.remove(content) + } + } + @JvmStatic fun tearDown() { if (instances.isNotEmpty()) { diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/OpenIDConnectAuthenticator.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/OpenIDConnectAuthenticator.kt index 22b5f7c6d6..f77c0b0ee4 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/OpenIDConnectAuthenticator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/OpenIDConnectAuthenticator.kt @@ -32,7 +32,6 @@ import javax.inject.Inject import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response -import org.hisp.dhis.android.core.arch.api.authentication.internal.UserIdAuthenticatorHelper.Companion.AUTHORIZATION_KEY import org.hisp.dhis.android.core.arch.storage.internal.Credentials import org.hisp.dhis.android.core.arch.storage.internal.CredentialsSecureStore import org.hisp.dhis.android.core.user.openid.OpenIDConnectLogoutHandler @@ -70,6 +69,9 @@ internal class OpenIDConnectAuthenticator @Inject constructor( } private fun addTokenHeader(builder: Request.Builder, token: String): Request.Builder { - return builder.addHeader(AUTHORIZATION_KEY, "Bearer $token") + return builder.addHeader( + UserIdAuthenticatorHelper.AUTHORIZATION_KEY, + "Bearer $token" + ) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/PasswordAndCookieAuthenticator.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/PasswordAndCookieAuthenticator.kt index dac0bce87e..7d96ba3eb5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/PasswordAndCookieAuthenticator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/PasswordAndCookieAuthenticator.kt @@ -32,8 +32,6 @@ import javax.inject.Inject import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response -import org.hisp.dhis.android.core.arch.api.authentication.internal.UserIdAuthenticatorHelper.Companion.AUTHORIZATION_KEY -import org.hisp.dhis.android.core.arch.helpers.UserHelper import org.hisp.dhis.android.core.arch.storage.internal.Credentials @Reusable @@ -73,11 +71,9 @@ internal class PasswordAndCookieAuthenticator @Inject constructor( } private fun addPasswordHeader(builder: Request.Builder, credentials: Credentials): Request.Builder { - return builder.addHeader(AUTHORIZATION_KEY, getAuthorizationForPassword(credentials)) - } - - private fun getAuthorizationForPassword(credentials: Credentials): String { - val base64Credentials = UserHelper.base64(credentials.username, credentials.password) - return "Basic $base64Credentials" + return builder.addHeader( + UserIdAuthenticatorHelper.AUTHORIZATION_KEY, + UserIdAuthenticatorHelper.basic(credentials) + ) } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/UserIdAuthenticatorHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/UserIdAuthenticatorHelper.kt index 932173cacb..72fa9883b1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/UserIdAuthenticatorHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/api/authentication/internal/UserIdAuthenticatorHelper.kt @@ -31,6 +31,8 @@ import dagger.Reusable import javax.inject.Inject import okhttp3.Interceptor import okhttp3.Request +import org.hisp.dhis.android.core.arch.helpers.UserHelper +import org.hisp.dhis.android.core.arch.storage.internal.Credentials import org.hisp.dhis.android.core.arch.storage.internal.UserIdInMemoryStore @Reusable @@ -41,6 +43,15 @@ internal class UserIdAuthenticatorHelper @Inject constructor( companion object { const val AUTHORIZATION_KEY = "Authorization" private const val USER_ID_KEY = "x-dhis2-user-id" + + fun basic(credentials: Credentials): String { + return basic(credentials.username, credentials.password!!) + } + + fun basic(username: String, password: String): String { + val base64Credentials = UserHelper.base64(username, password) + return "Basic $base64Credentials" + } } fun builderWithUserId(chain: Interceptor.Chain): Request.Builder { diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/api/executors/internal/APICallExecutorImpl.java b/core/src/main/java/org/hisp/dhis/android/core/arch/api/executors/internal/APICallExecutorImpl.java index 08d6789edb..f65f451066 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/api/executors/internal/APICallExecutorImpl.java +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/api/executors/internal/APICallExecutorImpl.java @@ -107,7 +107,7 @@ private

P executeObjectCallInternal(Call

call, } else if (errorCatcher != null) { this.catchAndThrow(errorCatcher, errorBuilder(call), response, errorBody); } - throw storeAndReturn(errorMapper.responseException(errorBuilder(call), response, errorBody)); + throw storeAndReturn(errorMapper.responseException(errorBuilder(call), response, null, errorBody)); } } catch (D2Error d2Error) { throw d2Error; @@ -136,7 +136,7 @@ private

P processSuccessfulResponse(D2Error.Builder errorBuilder, Response

socketTimeoutException(errorBuilder, throwable) is UnknownHostException -> unknownHostException(errorBuilder, throwable) + is ConnectException -> connectException(errorBuilder, throwable) is HttpException -> httpException(errorBuilder, throwable) is SSLException -> sslException(errorBuilder, throwable) is IOException -> ioException(errorBuilder, throwable) @@ -78,6 +80,13 @@ internal class APIErrorMapper @Inject constructor() { .build() } + private fun connectException(errorBuilder: D2Error.Builder, e: ConnectException): D2Error { + return logAndAppendOriginal(errorBuilder, e) + .errorCode(D2ErrorCode.SERVER_CONNECTION_ERROR) + .errorDescription("API call failed due to a ConnectException.") + .build() + } + private fun sslException(errorBuilder: D2Error.Builder, sslException: SSLException): D2Error { return logAndAppendOriginal(errorBuilder, sslException) .errorDescription(sslException.message) @@ -130,17 +139,17 @@ internal class APIErrorMapper @Inject constructor() { } } - @JvmOverloads fun responseException( errorBuilder: D2Error.Builder, response: Response<*>, - errorCode: D2ErrorCode? = D2ErrorCode.API_UNSUCCESSFUL_RESPONSE, + errorCode: D2ErrorCode?, errorBody: String? ): D2Error { + val code = errorCode ?: D2ErrorCode.API_UNSUCCESSFUL_RESPONSE val serverMessage = errorBody ?: getServerMessage(response) Log.e(this.javaClass.simpleName, serverMessage) return errorBuilder - .errorCode(errorCode) + .errorCode(code) .httpErrorCode(response.code()) .errorDescription("API call failed, server message: $serverMessage") .build() diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/D2ErrorCodeColumnAdapter.java b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/D2ErrorCodeColumnAdapter.java index d85738ca6f..48ddea44aa 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/D2ErrorCodeColumnAdapter.java +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/adapters/enums/internal/D2ErrorCodeColumnAdapter.java @@ -28,6 +28,9 @@ package org.hisp.dhis.android.core.arch.db.adapters.enums.internal; +import android.content.ContentValues; +import android.database.Cursor; + import org.hisp.dhis.android.core.maintenance.D2ErrorCode; public class D2ErrorCodeColumnAdapter extends EnumColumnAdapter { @@ -35,4 +38,19 @@ public class D2ErrorCodeColumnAdapter extends EnumColumnAdapter { protected Class getEnumClass() { return D2ErrorCode.class; } + + @Override + public D2ErrorCode fromCursor(Cursor cursor, String columnName) { + D2ErrorCode d2ErrorCode = super.fromCursor(cursor, columnName); + return d2ErrorCode == null ? D2ErrorCode.UNEXPECTED : d2ErrorCode; + } + + @Override + public void toContentValues(ContentValues contentValues, String columnName, D2ErrorCode enumValue) { + if (enumValue == null) { + contentValues.put(columnName, D2ErrorCode.UNEXPECTED.name()); + } else { + contentValues.put(columnName, enumValue.name()); + } + } } \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/db/querybuilders/internal/WhereClauseBuilder.java b/core/src/main/java/org/hisp/dhis/android/core/arch/db/querybuilders/internal/WhereClauseBuilder.java index 38ae888865..dc8b02a56e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/db/querybuilders/internal/WhereClauseBuilder.java +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/db/querybuilders/internal/WhereClauseBuilder.java @@ -133,6 +133,10 @@ public WhereClauseBuilder appendInSubQuery(String column, String subQuery) { return appendKeyValue(column, subQuery, AND, IN, PARENTHESES_END); } + public WhereClauseBuilder appendNotInSubQuery(String column, String subQuery) { + return appendKeyValue(column, subQuery, AND, NOT_IN, PARENTHESES_END); + } + public WhereClauseBuilder appendOrInSubQuery(String column, String subQuery) { return appendKeyValue(column, subQuery, OR, IN, PARENTHESES_END); } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/helpers/UserHelper.java b/core/src/main/java/org/hisp/dhis/android/core/arch/helpers/UserHelper.kt similarity index 50% rename from core/src/main/java/org/hisp/dhis/android/core/arch/helpers/UserHelper.java rename to core/src/main/java/org/hisp/dhis/android/core/arch/helpers/UserHelper.kt index 5bd7aa5ad7..825c7fc35a 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/helpers/UserHelper.java +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/helpers/UserHelper.kt @@ -25,77 +25,71 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +package org.hisp.dhis.android.core.arch.helpers -package org.hisp.dhis.android.core.arch.helpers; - - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import okio.ByteString; - -public final class UserHelper { - - private UserHelper() { - // no instances - } +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import okio.ByteString +object UserHelper { /** - * Encode the given username and password to a base 64 {@link String}. + * Encode the given username and password to a base 64 [String]. * * @param username The username of the user account. * @param password The password of the user account. - * @return An encoded base 64 {@link String}. + * @return An encoded base 64 [String]. */ - public static String base64(String username, String password) { - return base64(username + ":" + password); + fun base64(username: String, password: String): String { + return base64(usernameAndPassword(username, password)) } /** - * Encode the given string to a base 64 {@link String}. + * Encode the given string to a base 64 [String]. * * @param value The value to encode - * @return An encoded base 64 {@link String}. + * @return An encoded base 64 [String]. */ - public static String base64(String value) { - byte[] bytes = value.getBytes(StandardCharsets.ISO_8859_1); - return ByteString.of(bytes).base64(); + @Suppress("SpreadOperator") + fun base64(value: String): String { + val bytes = value.toByteArray(StandardCharsets.UTF_8) + return ByteString.of(*bytes).base64() } /** - * Encode the given username and password to a MD5 {@link String}. + * Encode the given username and password to a MD5 [String]. * * @param username The username of the user account. * @param password The password of the user account. - * @return An encoded MD5 {@link String}. + * @return An encoded MD5 [String]. */ - @SuppressWarnings({"PMD.UseLocaleWithCaseConversions"}) - public static String md5(String username, String password) { - try { - String credentials = usernameAndPassword(username, password); - MessageDigest md = MessageDigest.getInstance("MD5"); - md.reset(); - md.update(credentials.getBytes(StandardCharsets.ISO_8859_1)); - return bytesToHex(md.digest()).toLowerCase(); - } catch (NoSuchAlgorithmException noSuchAlgorithmException) { + fun md5(username: String, password: String): String { + return try { + val credentials = usernameAndPassword(username, password) + val md = MessageDigest.getInstance("MD5") + md.reset() + md.update(credentials.toByteArray(StandardCharsets.UTF_8)) + bytesToHex(md.digest()).lowercase() + } catch (noSuchAlgorithmException: NoSuchAlgorithmException) { // noop. Every implementation of Java is required to support MD5 - throw new AssertionError(noSuchAlgorithmException); + throw AssertionError(noSuchAlgorithmException) } } - private static String bytesToHex(byte[] bytes) { - char[] hexArray = "0123456789ABCDEF".toCharArray(); - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + @Suppress("MagicNumber") + private fun bytesToHex(bytes: ByteArray): String { + val hexArray = "0123456789ABCDEF".toCharArray() + val hexChars = CharArray(bytes.size * 2) + + for (j in bytes.indices) { + val v = bytes[j].toInt() and 0xFF + hexChars[j * 2] = hexArray[v ushr 4] + hexChars[j * 2 + 1] = hexArray[v and 0x0F] } - return new String(hexChars); + return String(hexChars) } - private static String usernameAndPassword(String username, String password) { - return username + ":" + password; + private fun usernameAndPassword(username: String, password: String): String { + return "$username:$password" } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/arch/storage/internal/Credentials.kt b/core/src/main/java/org/hisp/dhis/android/core/arch/storage/internal/Credentials.kt index 46c7661018..09fa92a350 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/arch/storage/internal/Credentials.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/arch/storage/internal/Credentials.kt @@ -37,7 +37,7 @@ data class Credentials( val openIDConnectState: AuthState? ) { fun getHash(): String? { - return password.let { UserHelper.md5(username, it) } + return password?.let { UserHelper.md5(username, it) } } override fun equals(other: Any?) = diff --git a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventImportHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventImportHandler.kt index 1a10439b34..7e4243e189 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventImportHandler.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/event/internal/EventImportHandler.kt @@ -43,7 +43,6 @@ import org.hisp.dhis.android.core.imports.internal.EventImportSummary import org.hisp.dhis.android.core.imports.internal.TEIWebResponseHandlerSummary import org.hisp.dhis.android.core.imports.internal.TrackerImportConflictParser import org.hisp.dhis.android.core.imports.internal.TrackerImportConflictStore -import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityDataValueStore import org.hisp.dhis.android.core.tracker.importer.internal.JobReportEventHandler @Reusable @@ -54,7 +53,6 @@ internal class EventImportHandler @Inject constructor( private val trackerImportConflictParser: TrackerImportConflictParser, private val jobReportEventHandler: JobReportEventHandler, private val dataStatePropagator: DataStatePropagator, - private val trackedEntityDataValueStore: TrackedEntityDataValueStore ) { @Suppress("NestedBlockDepth") @@ -88,7 +86,7 @@ internal class EventImportHandler @Inject constructor( if (state == State.SYNCED && (handleAction == HandleAction.Update || handleAction == HandleAction.Insert) ) { - handleIfSynced(eventUid, state) + jobReportEventHandler.handleSyncedEvent(eventUid) } } } @@ -123,14 +121,6 @@ internal class EventImportHandler @Inject constructor( } } - private fun handleIfSynced( - eventUid: String, - state: State - ) { - jobReportEventHandler.handleEventNotes(eventUid, state) - trackedEntityDataValueStore.removeDeletedDataValuesByEvent(eventUid) - } - private fun storeEventImportConflicts( importSummary: EventImportSummary, enrollmentUid: String? diff --git a/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2Error.java b/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2Error.java index 8024732d28..c24fafef36 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2Error.java +++ b/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2Error.java @@ -85,7 +85,9 @@ public static Builder builder() { public abstract Builder toBuilder(); public boolean isOffline() { - return errorCode() == D2ErrorCode.SOCKET_TIMEOUT || errorCode() == D2ErrorCode.UNKNOWN_HOST; + return errorCode() == D2ErrorCode.SOCKET_TIMEOUT || + errorCode() == D2ErrorCode.UNKNOWN_HOST || + errorCode() == D2ErrorCode.SERVER_CONNECTION_ERROR; } @AutoValue.Builder @@ -106,9 +108,15 @@ public static abstract class Builder extends BaseObject.Builder { public abstract Builder created(Date created); abstract D2Error autoBuild(); + abstract D2ErrorCode errorCode(); public D2Error build() { this.created(new Date()); + try { + errorCode(); + } catch (IllegalStateException e) { + errorCode(D2ErrorCode.UNEXPECTED); + } return autoBuild(); } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2ErrorCode.java b/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2ErrorCode.java index 3d4c665de7..b62192d085 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2ErrorCode.java +++ b/core/src/main/java/org/hisp/dhis/android/core/maintenance/D2ErrorCode.java @@ -70,6 +70,7 @@ public enum D2ErrorCode { OWNERSHIP_ACCESS_DENIED, PROGRAM_ACCESS_CLOSED, SEARCH_GRID_PARSE, + SERVER_CONNECTION_ERROR, SERVER_URL_NULL, SERVER_URL_MALFORMED, SETTINGS_APP_NOT_SUPPORTED, diff --git a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageDataElementStore.java b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageDataElementStore.java index 96dd7e42ef..c5860bc43e 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageDataElementStore.java +++ b/core/src/main/java/org/hisp/dhis/android/core/program/internal/ProgramStageDataElementStore.java @@ -40,7 +40,7 @@ import androidx.annotation.NonNull; -final class ProgramStageDataElementStore { +public final class ProgramStageDataElementStore { private ProgramStageDataElementStore() {} @@ -62,7 +62,7 @@ public void bindToStatement(@NonNull ProgramStageDataElement programStageDataEle } }; - static IdentifiableObjectStore create(DatabaseAdapter databaseAdapter) { + public static IdentifiableObjectStore create(DatabaseAdapter databaseAdapter) { return StoreFactory.objectWithUidStore(databaseAdapter, ProgramStageDataElementTableInfo.TABLE_INFO, BINDER, ProgramStageDataElement::create); } diff --git a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipOrphanCleanerImpl.java b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipOrphanCleanerImpl.java index 30dbc2fb84..4267664c88 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipOrphanCleanerImpl.java +++ b/core/src/main/java/org/hisp/dhis/android/core/relationship/internal/RelationshipOrphanCleanerImpl.java @@ -84,7 +84,8 @@ private boolean isSynced(State state) { private boolean isInRelationshipList(Relationship target, Collection list) { for (Relationship relationship : list) { - if (target.from() == null || target.to() == null || relationship.from() == null || target.to() == null) { + if (target.from() == null || target.to() == null || + relationship.from() == null || relationship.to() == null) { continue; } if (areItemsEqual(target.from(), relationship.from()) && diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackerImporterPayloadGenerator.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackerImporterPayloadGenerator.kt index 21779cad58..8dc16f8e83 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackerImporterPayloadGenerator.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/OldTrackerImporterPayloadGenerator.kt @@ -70,7 +70,6 @@ internal class OldTrackerImporterPayloadGenerator @Inject internal constructor( ) { private data class ExtraData( - val dataValueMap: Map>, val eventMap: Map>, val enrollmentMap: Map>, val attributeValueMap: Map>, @@ -241,7 +240,6 @@ internal class OldTrackerImporterPayloadGenerator @Inject internal constructor( private fun getExtraData(): ExtraData { return ExtraData( - dataValueMap = trackedEntityDataValueStore.queryByUploadableEvents(), eventMap = eventStore.queryEventsAttachedToEnrollmentToPost(), enrollmentMap = enrollmentStore.queryEnrollmentsToPost(), attributeValueMap = trackedEntityAttributeValueStore.queryTrackedEntityAttributeValueToPost(), @@ -318,16 +316,21 @@ internal class OldTrackerImporterPayloadGenerator @Inject internal constructor( getEvent(event, extraData) } ?: emptyList() + val notes = extraData.notes.filter { it.enrollment() == enrollment.uid() } + EnrollmentInternalAccessor.insertEvents(enrollment.toBuilder(), events) - .notes(getEnrollmentNotes(extraData.notes, enrollment)) + .notes(notes) .build() } ?: emptyList() } private fun getEvent(event: Event, extraData: ExtraData): Event { + val eventDataValues = trackedEntityDataValueStore.queryToPostByEvent(event.uid()) + val eventNotes = extraData.notes.filter { it.event() == event.uid() } + val eventBuilder = event.toBuilder() - .trackedEntityDataValues(extraData.dataValueMap[event.uid()]) - .notes(getEventNotes(extraData.notes, event)) + .trackedEntityDataValues(eventDataValues) + .notes(eventNotes) if (versionManager.getVersion() == DHISVersion.V2_30) { eventBuilder.geometry(null) @@ -336,14 +339,6 @@ internal class OldTrackerImporterPayloadGenerator @Inject internal constructor( return eventBuilder.build() } - private fun getEventNotes(notes: List, event: Event): List { - return notes.filter { it.event() == event.uid() } - } - - private fun getEnrollmentNotes(notes: List, enrollment: Enrollment): List { - return notes.filter { it.enrollment() == enrollment.uid() } - } - private fun addProgramOwners(payload: OldTrackerImporterPayload): OldTrackerImporterPayload { return if (payload.trackedEntityInstances.isNotEmpty()) { val programOwnerWhere = WhereClauseBuilder() diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityDataValueStore.java b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityDataValueStore.java index 61fc96cbd9..ccc5838bd1 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityDataValueStore.java +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityDataValueStore.java @@ -48,7 +48,9 @@ public interface TrackedEntityDataValueStore extends ObjectWithoutUidStore> queryTrackerTrackedEntityDataValues(); - Map> queryByUploadableEvents(); + List queryToPostByEvent(@NonNull String eventUid); void removeDeletedDataValuesByEvent(@NonNull String eventUid); + + void removeUnassignedDataValuesByEvent(@NonNull String eventUid); } \ No newline at end of file diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityDataValueStoreImpl.java b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityDataValueStoreImpl.java index 1860f5d03a..45a204fe80 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityDataValueStoreImpl.java +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/internal/TrackedEntityDataValueStoreImpl.java @@ -43,6 +43,7 @@ import org.hisp.dhis.android.core.arch.helpers.internal.EnumHelper; import org.hisp.dhis.android.core.common.State; import org.hisp.dhis.android.core.event.EventTableInfo; +import org.hisp.dhis.android.core.program.ProgramStageDataElementTableInfo; import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValueTableInfo; import org.hisp.dhis.android.core.trackedentity.TrackedEntityDataValue; import org.hisp.dhis.android.core.trackedentity.TrackedEntityDataValueTableInfo; @@ -157,13 +158,39 @@ public Map> queryTrackerTrackedEntityDataVa } @Override - public Map> queryByUploadableEvents() { + public List queryToPostByEvent(@NonNull String eventUid) { + String queryStatement = new WhereClauseBuilder() + .appendKeyStringValue(TrackedEntityDataValueTableInfo.Columns.EVENT, eventUid) + .appendInSubQuery( + TrackedEntityDataValueTableInfo.Columns.DATA_ELEMENT, + getInProgramStageDataElementsSubQuery(eventUid) + ).build(); - String queryStatement = "SELECT TrackedEntityDataValue.* " + - " FROM (TrackedEntityDataValue INNER JOIN Event ON TrackedEntityDataValue.event = Event.uid) " + - " WHERE " + eventInUploadableState() + ";"; + return selectWhere(queryStatement); + } - return queryTrackedEntityDataValues(queryStatement); + @Override + public void removeUnassignedDataValuesByEvent(@NonNull String eventUid) { + String queryStatement = new WhereClauseBuilder() + .appendKeyStringValue(TrackedEntityDataValueTableInfo.Columns.EVENT, eventUid) + .appendNotInSubQuery( + TrackedEntityDataValueTableInfo.Columns.DATA_ELEMENT, + getInProgramStageDataElementsSubQuery(eventUid) + ).build(); + + deleteWhere(queryStatement); + } + + private String getInProgramStageDataElementsSubQuery(@NonNull String eventUid) { + String psDataElementName = ProgramStageDataElementTableInfo.TABLE_INFO.name(); + String eventName = EventTableInfo.TABLE_INFO.name(); + + return "SELECT " + ProgramStageDataElementTableInfo.Columns.DATA_ELEMENT + + " FROM " + psDataElementName + + " INNER JOIN " + eventName + + " ON " + psDataElementName + "." + ProgramStageDataElementTableInfo.Columns.PROGRAM_STAGE + + " = " + eventName + "." + EventTableInfo.Columns.PROGRAM_STAGE + + " WHERE " + eventName + "." + EventTableInfo.Columns.UID + " = '" + eventUid + "'"; } private Map> queryTrackedEntityDataValues(String queryStatement) { diff --git a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt index b9c390717c..9646a491c5 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelper.kt @@ -157,7 +157,11 @@ internal class TrackedEntityInstanceQueryOnlineHelper @Inject constructor( } companion object { + fun toAPIFilterFormat(items: List, upper: Boolean): List { + // Compatibility for the new Tracker (old Tracker will ignore this format) + // Following characters need to be escaped escaped with a "/" for backend functionality + return items .groupBy { it.key() } .map { (key, items) -> @@ -172,12 +176,19 @@ internal class TrackedEntityInstanceQueryOnlineHelper @Inject constructor( } private fun getAPIValue(item: RepositoryScopeFilterItem): String { + return if (item.operator() == FilterItemOperator.IN) { - val list = FilterOperatorsHelper.strToList(item.value()) + val list = FilterOperatorsHelper.strToList(item.value()).map { escapeChars(it) } list.joinToString(";") } else { - item.value() + escapeChars(item.value()) } } + + private fun escapeChars(targetString: String): String { + val charsToEscape = "[;,:]".toRegex() + val escapingChar = "/\$0" + return targetString.replace(charsToEscape, escapingChar) + } } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/tracker/importer/internal/JobReportEventHandler.kt b/core/src/main/java/org/hisp/dhis/android/core/tracker/importer/internal/JobReportEventHandler.kt index 17f912cf71..04f3a61f16 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/tracker/importer/internal/JobReportEventHandler.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/tracker/importer/internal/JobReportEventHandler.kt @@ -55,25 +55,12 @@ internal class JobReportEventHandler @Inject internal constructor( relationshipStore: RelationshipStore ) : JobReportTypeHandler(relationshipStore) { - fun handleEventNotes(eventUid: String, state: State) { - val newNoteState = if (state == State.SYNCED) State.SYNCED else State.TO_POST - val whereClause = WhereClauseBuilder() - .appendInKeyStringValues( - DataColumns.SYNC_STATE, State.uploadableStatesIncludingError().map { it.name } - ) - .appendKeyStringValue(NoteTableInfo.Columns.EVENT, eventUid).build() - for (note in noteStore.selectWhere(whereClause)) { - noteStore.update(note.toBuilder().syncState(newNoteState).build()) - } - } - override fun handleObject(uid: String, state: State): HandleAction { conflictStore.deleteEventConflicts(uid) val handleAction = eventStore.setSyncStateOrDelete(uid, state) if (state == State.SYNCED && (handleAction == HandleAction.Update || handleAction == HandleAction.Insert)) { - handleEventNotes(uid, state) - trackedEntityDataValueStore.removeDeletedDataValuesByEvent(uid) + handleSyncedEvent(uid) } return handleAction @@ -102,4 +89,22 @@ internal class JobReportEventHandler @Inject internal constructor( override fun getRelatedRelationships(uid: String): List { return relationshipStore.getRelationshipsByItem(RelationshipHelper.eventItem(uid)).mapNotNull { it.uid() } } + + fun handleSyncedEvent(eventUid: String) { + handleEventNotes(eventUid, State.SYNCED) + trackedEntityDataValueStore.removeDeletedDataValuesByEvent(eventUid) + trackedEntityDataValueStore.removeUnassignedDataValuesByEvent(eventUid) + } + + private fun handleEventNotes(eventUid: String, state: State) { + val newNoteState = if (state == State.SYNCED) State.SYNCED else State.TO_POST + val whereClause = WhereClauseBuilder() + .appendInKeyStringValues( + DataColumns.SYNC_STATE, State.uploadableStatesIncludingError().map { it.name } + ) + .appendKeyStringValue(NoteTableInfo.Columns.EVENT, eventUid).build() + for (note in noteStore.selectWhere(whereClause)) { + noteStore.update(note.toBuilder().syncState(newNoteState).build()) + } + } } diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInCall.kt b/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInCall.kt index a095eb7ce1..f2bc238cd6 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInCall.kt +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/LogInCall.kt @@ -31,6 +31,7 @@ import dagger.Reusable import io.reactivex.Single import javax.inject.Inject import net.openid.appauth.AuthState +import org.hisp.dhis.android.core.arch.api.authentication.internal.UserIdAuthenticatorHelper import org.hisp.dhis.android.core.arch.api.executors.internal.APICallExecutor import org.hisp.dhis.android.core.arch.api.internal.ServerURLWrapper import org.hisp.dhis.android.core.arch.db.access.DatabaseAdapter @@ -87,7 +88,7 @@ internal class LogInCall @Inject internal constructor( ServerURLWrapper.setServerUrl(parsedServerUrl.toString()) val authenticateCall = userService.authenticate( - okhttp3.Credentials.basic(username!!, password!!), + UserIdAuthenticatorHelper.basic(username!!, password!!), UserFields.allFieldsWithoutOrgUnit(null) ) diff --git a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserService.java b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserService.java index d3363fcf94..0e2b651453 100644 --- a/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserService.java +++ b/core/src/main/java/org/hisp/dhis/android/core/user/internal/UserService.java @@ -28,6 +28,7 @@ package org.hisp.dhis.android.core.user.internal; +import org.hisp.dhis.android.core.arch.api.authentication.internal.UserIdAuthenticatorHelper; import org.hisp.dhis.android.core.arch.api.fields.internal.Fields; import org.hisp.dhis.android.core.arch.api.filters.internal.Which; import org.hisp.dhis.android.core.user.User; @@ -40,7 +41,7 @@ interface UserService { @GET("me") - Call authenticate(@Header("Authorization") String credentials, + Call authenticate(@Header(UserIdAuthenticatorHelper.AUTHORIZATION_KEY) String credentials, @Query("fields") @Which Fields fields); @GET("me") diff --git a/core/src/test/java/org/hisp/dhis/android/core/arch/helpers/UserHelperShould.kt b/core/src/test/java/org/hisp/dhis/android/core/arch/helpers/UserHelperShould.kt new file mode 100644 index 0000000000..b6d74b25e6 --- /dev/null +++ b/core/src/test/java/org/hisp/dhis/android/core/arch/helpers/UserHelperShould.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.android.core.arch.helpers + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class UserHelperShould { + @Test + fun md5_evaluate_same_string() { + val md5s1 = UserHelper.md5("user1", "password1") + val md5s2 = UserHelper.md5("user1", "password1") + + assertThat(md5s1.length).isEqualTo(32) + assertThat(md5s2.length).isEqualTo(32) + assertThat(md5s1 == md5s2).isTrue() + } + + @Test + fun md5_evaluate_different_string() { + val md5s1 = UserHelper.md5("user2", "password2") + val md5s2 = UserHelper.md5("user3", "password3") + + assertThat(md5s1.length).isEqualTo(32) + assertThat(md5s2.length).isEqualTo(32) + assertThat(md5s1 == md5s2).isFalse() + } + + @Test + fun md5_evaluate_special_chars() { + val md5s1 = UserHelper.md5("user1", "pässword") + val md5s2 = UserHelper.md5("user1", "password") + + assertThat(md5s1.length).isEqualTo(32) + assertThat(md5s2.length).isEqualTo(32) + assertThat(md5s1 == md5s2).isFalse() + } + + @Test + fun base64_encode_credentials() { + val base64 = UserHelper.base64("user", "password") + assertThat(base64).isEqualTo("dXNlcjpwYXNzd29yZA==") + } + + @Test + fun base64_encode_special_chars() { + val base64 = UserHelper.base64("user", "pässword") + assertThat(base64).isEqualTo("dXNlcjpww6Rzc3dvcmQ=") + } +} diff --git a/core/src/test/java/org/hisp/dhis/android/core/event/internal/EventImportHandlerShould.kt b/core/src/test/java/org/hisp/dhis/android/core/event/internal/EventImportHandlerShould.kt index e490d0d75f..a4cfc0a6e6 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/event/internal/EventImportHandlerShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/event/internal/EventImportHandlerShould.kt @@ -36,7 +36,6 @@ import org.hisp.dhis.android.core.imports.ImportStatus import org.hisp.dhis.android.core.imports.internal.EventImportSummary import org.hisp.dhis.android.core.imports.internal.TrackerImportConflictParser import org.hisp.dhis.android.core.imports.internal.TrackerImportConflictStore -import org.hisp.dhis.android.core.trackedentity.internal.TrackedEntityDataValueStore import org.hisp.dhis.android.core.tracker.importer.internal.JobReportEventHandler import org.junit.Before import org.junit.Test @@ -60,8 +59,6 @@ class EventImportHandlerShould { private val trackerImportConflictParser: TrackerImportConflictParser = mock() - private val trackedEntityDataValueStore: TrackedEntityDataValueStore = mock() - private val events: List = ArrayList() private val event: Event = mock() @@ -76,7 +73,7 @@ class EventImportHandlerShould { eventImportHandler = EventImportHandler( eventStore, enrollmentStore, trackerImportConflictStore, trackerImportConflictParser, jobReportEventHandler, - dataStatePropagator, trackedEntityDataValueStore + dataStatePropagator ) } diff --git a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt index 1f1fb53d23..edf86876a3 100644 --- a/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt +++ b/core/src/test/java/org/hisp/dhis/android/core/trackedentity/search/TrackedEntityInstanceQueryOnlineHelperShould.kt @@ -34,6 +34,7 @@ import org.hisp.dhis.android.core.common.DateFilterPeriodHelper import org.hisp.dhis.android.core.common.FilterOperatorsHelper.listToStr import org.hisp.dhis.android.core.period.internal.CalendarProviderFactory.calendarProvider import org.hisp.dhis.android.core.period.internal.ParentPeriodGeneratorImpl.Companion.create +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntityInstanceQueryOnlineHelper.Companion.toAPIFilterFormat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -85,4 +86,39 @@ class TrackedEntityInstanceQueryOnlineHelperShould { assertThat(onlineQueries.size).isEqualTo(1) assertThat(onlineQueries[0].attributeFilter.size).isEqualTo(1) } + + @Test + fun to_API_filter_format() { + // List of filters + val list = listOf( + "nom,app", + "nom:app", + "nom;app" + ) + + val expectedList = listOf( + "filterItemIN:in:nom/,app;nom/:app;nom/;app", + "filterItemLIKE1:like:nom/,app", + "filterItemLIKE2:like:nom/:app", + "filterItemLIKE3:like:nom/;app", + ) + + val scope = queryBuilder + .filter( + listOf( + RepositoryScopeFilterItem.builder() + .key("filterItemIN").operator(FilterItemOperator.IN).value(listToStr(list)).build(), + RepositoryScopeFilterItem.builder() + .key("filterItemLIKE1").operator(FilterItemOperator.LIKE).value(list[0]).build(), + RepositoryScopeFilterItem.builder() + .key("filterItemLIKE2").operator(FilterItemOperator.LIKE).value(list[1]).build(), + RepositoryScopeFilterItem.builder() + .key("filterItemLIKE3").operator(FilterItemOperator.LIKE).value(list[2]).build() + ) + ).build() + + val formattedQueries = toAPIFilterFormat(scope.filter(), false) + + assertThat(formattedQueries).isEqualTo(expectedList) + } } diff --git a/docs/content/developer/getting-started.md b/docs/content/developer/getting-started.md index 59ab7d2742..50d96b6bdf 100644 --- a/docs/content/developer/getting-started.md +++ b/docs/content/developer/getting-started.md @@ -6,7 +6,7 @@ Include dependency in build.gradle. ```gradle dependencies { - implementation "org.hisp.dhis:android-core:1.8.1.1" + implementation "org.hisp.dhis:android-core:1.8.2" ... } ```