diff --git a/.gitignore b/.gitignore index eaf561b448f..dce48393a57 100644 --- a/.gitignore +++ b/.gitignore @@ -39,9 +39,6 @@ collect_app/.classpath collect_app/.project collect_app/.settings/ -# Submodules -javarosa/ - # Config for the official ODK Collect build collect_app/src/odkCollectRelease/ diff --git a/buildSrc/src/main/java/dependencies/Dependencies.kt b/buildSrc/src/main/java/dependencies/Dependencies.kt index 0e668302e5d..58ce3bded09 100644 --- a/buildSrc/src/main/java/dependencies/Dependencies.kt +++ b/buildSrc/src/main/java/dependencies/Dependencies.kt @@ -35,7 +35,7 @@ object Dependencies { const val rarepebble_colorpicker = "com.github.martin-stone:hsv-alpha-color-picker-android:3.1.0" const val commons_io = "commons-io:commons-io:2.5" // Commons 2.6+ introduce java.nio usage that we can't access until our minSdkVersion >= 26 (https://developer.android.com/reference/java/io/File#toPath()) const val opencsv = "com.opencsv:opencsv:5.9" - const val javarosa_online = "org.getodk:javarosa:4.4.0" + const val javarosa_online = "org.getodk:javarosa:5.0.0-SNAPSHOT" const val javarosa_local = "org.getodk:javarosa:local" const val javarosa = javarosa_online const val karumi_dexter = "com.karumi:dexter:6.2.3" diff --git a/collect_app/proguard-rules.txt b/collect_app/proguard-rules.txt index 19ace210362..7bd49b8f6f8 100644 --- a/collect_app/proguard-rules.txt +++ b/collect_app/proguard-rules.txt @@ -9,13 +9,14 @@ -dontwarn android.content.res.** -dontwarn org.kxml2.io.** --keep class org.javarosa.** -keep class org.odk.collect.android.logic.actions.** -keep class android.support.v7.widget.** { *; } -keep class org.mp4parser.boxes.** { *; } +-keep class * extends androidx.fragment.app.Fragment{} + +-keep class * implements org.javarosa.core.util.externalizable.Externalizable{} -keep class org.javarosa.core.model.instance.geojson.GeojsonFeature { *; } -keep class org.javarosa.core.model.instance.geojson.GeojsonGeometry { *; } --keep class * extends androidx.fragment.app.Fragment{} -dontobfuscate diff --git a/collect_app/src/main/AndroidManifest.xml b/collect_app/src/main/AndroidManifest.xml index 3b61c7a5575..00cb93ce1cd 100644 --- a/collect_app/src/main/AndroidManifest.xml +++ b/collect_app/src/main/AndroidManifest.xml @@ -160,7 +160,7 @@ the specific language governing permissions and limitations under the License. tools:replace="android:theme" /> diff --git a/collect_app/src/main/java/org/odk/collect/android/application/Collect.java b/collect_app/src/main/java/org/odk/collect/android/application/Collect.java index 66a6bb22b99..ddecf584b2c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/application/Collect.java +++ b/collect_app/src/main/java/org/odk/collect/android/application/Collect.java @@ -55,7 +55,7 @@ import org.odk.collect.entities.EntitiesDependencyComponent; import org.odk.collect.entities.EntitiesDependencyComponentProvider; import org.odk.collect.entities.EntitiesDependencyModule; -import org.odk.collect.entities.EntitiesRepository; +import org.odk.collect.entities.storage.EntitiesRepository; import org.odk.collect.forms.Form; import org.odk.collect.geo.DaggerGeoDependencyComponent; import org.odk.collect.geo.GeoDependencyComponent; diff --git a/collect_app/src/main/java/org/odk/collect/android/application/initialization/JavaRosaInitializer.kt b/collect_app/src/main/java/org/odk/collect/android/application/initialization/JavaRosaInitializer.kt index a140ab0c781..1ca04f4ef1c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/application/initialization/JavaRosaInitializer.kt +++ b/collect_app/src/main/java/org/odk/collect/android/application/initialization/JavaRosaInitializer.kt @@ -3,7 +3,6 @@ package org.odk.collect.android.application.initialization import org.javarosa.core.model.CoreModelModule import org.javarosa.core.services.PrototypeManager import org.javarosa.core.util.JavaRosaCoreModule -import org.javarosa.entities.EntityXFormParserFactory import org.javarosa.model.xform.XFormsModule import org.javarosa.xform.parse.XFormParser import org.javarosa.xform.parse.XFormParserFactory @@ -11,7 +10,8 @@ import org.javarosa.xform.util.XFormUtils import org.odk.collect.android.dynamicpreload.DynamicPreloadXFormParserFactory import org.odk.collect.android.entities.EntitiesRepositoryProvider import org.odk.collect.android.logic.actions.setgeopoint.CollectSetGeopointActionHandler -import org.odk.collect.entities.LocalEntitiesExternalInstanceParserFactory +import org.odk.collect.entities.javarosa.intance.LocalEntitiesExternalInstanceParserFactory +import org.odk.collect.entities.javarosa.parse.EntityXFormParserFactory import org.odk.collect.metadata.PropertyManager import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.keys.ProjectKeys @@ -40,7 +40,10 @@ class JavaRosaInitializer( ) // Configure default parser factory - val entityXFormParserFactory = EntityXFormParserFactory(XFormParserFactory()) + val entityXFormParserFactory = + EntityXFormParserFactory( + XFormParserFactory() + ) val dynamicPreloadXFormParserFactory = DynamicPreloadXFormParserFactory(entityXFormParserFactory) diff --git a/collect_app/src/main/java/org/odk/collect/android/entities/EntitiesRepositoryProvider.kt b/collect_app/src/main/java/org/odk/collect/android/entities/EntitiesRepositoryProvider.kt index d76ff7dc4d7..24a97883861 100644 --- a/collect_app/src/main/java/org/odk/collect/android/entities/EntitiesRepositoryProvider.kt +++ b/collect_app/src/main/java/org/odk/collect/android/entities/EntitiesRepositoryProvider.kt @@ -2,7 +2,7 @@ package org.odk.collect.android.entities import org.odk.collect.android.projects.ProjectsDataService import org.odk.collect.android.storage.StoragePathProvider -import org.odk.collect.entities.EntitiesRepository +import org.odk.collect.entities.storage.EntitiesRepository import java.io.File class EntitiesRepositoryProvider( diff --git a/collect_app/src/main/java/org/odk/collect/android/entities/JsonFileEntitiesRepository.kt b/collect_app/src/main/java/org/odk/collect/android/entities/JsonFileEntitiesRepository.kt index a9d0d92eeb0..bb32040471e 100644 --- a/collect_app/src/main/java/org/odk/collect/android/entities/JsonFileEntitiesRepository.kt +++ b/collect_app/src/main/java/org/odk/collect/android/entities/JsonFileEntitiesRepository.kt @@ -3,8 +3,8 @@ package org.odk.collect.android.entities import android.os.StrictMode import com.google.gson.Gson import com.google.gson.reflect.TypeToken -import org.odk.collect.entities.EntitiesRepository -import org.odk.collect.entities.Entity +import org.odk.collect.entities.storage.EntitiesRepository +import org.odk.collect.entities.storage.Entity import java.io.File class JsonFileEntitiesRepository(directory: File) : EntitiesRepository { diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryUseCases.kt b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryUseCases.kt index 2899260f7a5..66a99e00a6e 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryUseCases.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEntryUseCases.kt @@ -15,8 +15,8 @@ import org.odk.collect.android.javarosawrapper.FormController import org.odk.collect.android.javarosawrapper.JavaRosaFormController import org.odk.collect.android.utilities.FileUtils import org.odk.collect.android.utilities.FormUtils -import org.odk.collect.entities.EntitiesRepository import org.odk.collect.entities.LocalEntityUseCases +import org.odk.collect.entities.storage.EntitiesRepository import org.odk.collect.forms.Form import org.odk.collect.forms.FormsRepository import org.odk.collect.forms.instances.Instance diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/DiskFormSaver.java b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/DiskFormSaver.java index 4c795e0bbae..f5b12e6316b 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/DiskFormSaver.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/DiskFormSaver.java @@ -6,7 +6,7 @@ import org.odk.collect.android.tasks.SaveFormToDisk; import org.odk.collect.android.tasks.SaveToDiskResult; import org.odk.collect.android.utilities.MediaUtils; -import org.odk.collect.entities.EntitiesRepository; +import org.odk.collect.entities.storage.EntitiesRepository; import org.odk.collect.forms.instances.InstancesRepository; import java.util.ArrayList; diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java index f31e6be7281..88d50bdcc99 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java @@ -34,7 +34,7 @@ import org.odk.collect.async.Cancellable; import org.odk.collect.async.Scheduler; import org.odk.collect.audiorecorder.recording.AudioRecorder; -import org.odk.collect.entities.EntitiesRepository; +import org.odk.collect.entities.storage.EntitiesRepository; import org.odk.collect.forms.Form; import org.odk.collect.forms.instances.Instance; import org.odk.collect.forms.instances.InstancesRepository; diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaver.java b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaver.java index 8aff5c68407..34f94f9c113 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaver.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaver.java @@ -5,7 +5,7 @@ import org.odk.collect.android.javarosawrapper.FormController; import org.odk.collect.android.tasks.SaveToDiskResult; import org.odk.collect.android.utilities.MediaUtils; -import org.odk.collect.entities.EntitiesRepository; +import org.odk.collect.entities.storage.EntitiesRepository; import org.odk.collect.forms.instances.InstancesRepository; import java.util.ArrayList; diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/CollectFormEntryControllerFactory.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/CollectFormEntryControllerFactory.kt index 751748e98e0..2a1b19d3feb 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/CollectFormEntryControllerFactory.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/CollectFormEntryControllerFactory.kt @@ -1,13 +1,13 @@ package org.odk.collect.android.formmanagement import org.javarosa.core.model.FormDef -import org.javarosa.entities.EntityFormFinalizationProcessor import org.javarosa.form.api.FormEntryController import org.javarosa.form.api.FormEntryModel import org.odk.collect.android.application.Collect import org.odk.collect.android.dynamicpreload.ExternalDataManagerImpl import org.odk.collect.android.dynamicpreload.handler.ExternalDataHandlerPull import org.odk.collect.android.tasks.FormLoaderTask.FormEntryControllerFactory +import org.odk.collect.entities.javarosa.finalization.EntityFormFinalizationProcessor import java.io.File class CollectFormEntryControllerFactory : diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt index 53480f30d30..63ceed05fed 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt @@ -4,8 +4,8 @@ import org.odk.collect.android.formmanagement.download.FormDownloadException import org.odk.collect.android.formmanagement.download.FormDownloader import org.odk.collect.android.utilities.FileUtils import org.odk.collect.async.OngoingWorkListener -import org.odk.collect.entities.EntitiesRepository import org.odk.collect.entities.LocalEntityUseCases +import org.odk.collect.entities.storage.EntitiesRepository import org.odk.collect.forms.Form import org.odk.collect.forms.FormSource import org.odk.collect.forms.FormSourceException diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/download/ServerFormDownloader.java b/collect_app/src/main/java/org/odk/collect/android/formmanagement/download/ServerFormDownloader.java index 61cba25d64a..1e9e318d933 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/download/ServerFormDownloader.java +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/download/ServerFormDownloader.java @@ -11,7 +11,7 @@ import org.odk.collect.android.utilities.FormNameUtils; import org.odk.collect.androidshared.utils.Validator; import org.odk.collect.async.OngoingWorkListener; -import org.odk.collect.entities.EntitiesRepository; +import org.odk.collect.entities.storage.EntitiesRepository; import org.odk.collect.forms.Form; import org.odk.collect.forms.FormSource; import org.odk.collect.forms.FormSourceException; diff --git a/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormController.kt b/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormController.kt index 97b4dd63009..99dd7129f2e 100644 --- a/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormController.kt +++ b/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/FormController.kt @@ -5,11 +5,11 @@ import org.javarosa.core.model.FormIndex import org.javarosa.core.model.data.IAnswerData import org.javarosa.core.model.instance.TreeReference import org.javarosa.core.services.transport.payload.ByteArrayPayload -import org.javarosa.entities.internal.Entities import org.javarosa.form.api.FormEntryCaption import org.javarosa.form.api.FormEntryPrompt import org.odk.collect.android.exception.JavaRosaException import org.odk.collect.android.formentry.audit.AuditEventLogger +import org.odk.collect.entities.javarosa.finalization.EntitiesExtra import java.io.File import java.io.IOException @@ -334,5 +334,5 @@ interface FormController { fun getAnswer(treeReference: TreeReference?): IAnswerData? - fun getEntities(): Entities? + fun getEntities(): EntitiesExtra? } diff --git a/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/JavaRosaFormController.java b/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/JavaRosaFormController.java index 457e81a7934..a739f0c29d8 100644 --- a/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/JavaRosaFormController.java +++ b/collect_app/src/main/java/org/odk/collect/android/javarosawrapper/JavaRosaFormController.java @@ -37,7 +37,6 @@ import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.services.transport.payload.ByteArrayPayload; -import org.javarosa.entities.internal.Entities; import org.javarosa.form.api.FormEntryCaption; import org.javarosa.form.api.FormEntryController; import org.javarosa.form.api.FormEntryModel; @@ -54,6 +53,7 @@ import org.odk.collect.android.formentry.audit.AuditEventLogger; import org.odk.collect.android.utilities.Appearances; import org.odk.collect.android.utilities.FileUtils; +import org.odk.collect.entities.javarosa.finalization.EntitiesExtra; import java.io.File; import java.io.IOException; @@ -1109,7 +1109,7 @@ public IAnswerData getAnswer(TreeReference treeReference) { return getFormDef().getMainInstance().resolveReference(treeReference).getValue(); } - public Entities getEntities() { - return formEntryController.getModel().getExtras().get(Entities.class); + public EntitiesExtra getEntities() { + return formEntryController.getModel().getExtras().get(EntitiesExtra.class); } } diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/ExperimentalPreferencesFragment.java b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/ExperimentalPreferencesFragment.java index c5ca63777dd..f440c90cabc 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/ExperimentalPreferencesFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/ExperimentalPreferencesFragment.java @@ -10,7 +10,7 @@ import org.jetbrains.annotations.NotNull; import org.odk.collect.android.R; import org.odk.collect.androidshared.ui.ToastUtils; -import org.odk.collect.entities.EntityBrowserActivity; +import org.odk.collect.entities.browser.EntityBrowserActivity; public class ExperimentalPreferencesFragment extends BaseProjectPreferencesFragment { diff --git a/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java b/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java index c40fd464a0a..ce121d5d6e8 100644 --- a/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java +++ b/collect_app/src/main/java/org/odk/collect/android/tasks/SaveFormToDisk.java @@ -56,7 +56,7 @@ import org.odk.collect.android.utilities.FileUtils; import org.odk.collect.android.utilities.FormsRepositoryProvider; import org.odk.collect.android.utilities.MediaUtils; -import org.odk.collect.entities.EntitiesRepository; +import org.odk.collect.entities.storage.EntitiesRepository; import org.odk.collect.forms.Form; import org.odk.collect.forms.instances.Instance; import org.odk.collect.forms.instances.InstancesRepository; diff --git a/collect_app/src/test/java/org/odk/collect/android/entities/EntitiesRepositoryTest.kt b/collect_app/src/test/java/org/odk/collect/android/entities/EntitiesRepositoryTest.kt index 7ffff307686..b3f3b3041da 100644 --- a/collect_app/src/test/java/org/odk/collect/android/entities/EntitiesRepositoryTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/entities/EntitiesRepositoryTest.kt @@ -5,8 +5,8 @@ import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.containsInAnyOrder import org.hamcrest.Matchers.equalTo import org.junit.Test -import org.odk.collect.entities.EntitiesRepository -import org.odk.collect.entities.Entity +import org.odk.collect.entities.storage.EntitiesRepository +import org.odk.collect.entities.storage.Entity abstract class EntitiesRepositoryTest { diff --git a/collect_app/src/test/java/org/odk/collect/android/entities/InMemEntitiesRepositoryTest.kt b/collect_app/src/test/java/org/odk/collect/android/entities/InMemEntitiesRepositoryTest.kt index ac623d393b7..bdd19cd140c 100644 --- a/collect_app/src/test/java/org/odk/collect/android/entities/InMemEntitiesRepositoryTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/entities/InMemEntitiesRepositoryTest.kt @@ -1,7 +1,7 @@ package org.odk.collect.android.entities -import org.odk.collect.entities.EntitiesRepository -import org.odk.collect.entities.InMemEntitiesRepository +import org.odk.collect.entities.storage.EntitiesRepository +import org.odk.collect.entities.storage.InMemEntitiesRepository class InMemEntitiesRepositoryTest : EntitiesRepositoryTest() { diff --git a/collect_app/src/test/java/org/odk/collect/android/entities/JsonFileEntitiesRepositoryTest.kt b/collect_app/src/test/java/org/odk/collect/android/entities/JsonFileEntitiesRepositoryTest.kt index d54a3fc8f5f..372ef44fda4 100644 --- a/collect_app/src/test/java/org/odk/collect/android/entities/JsonFileEntitiesRepositoryTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/entities/JsonFileEntitiesRepositoryTest.kt @@ -4,8 +4,8 @@ import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.equalTo import org.junit.Test -import org.odk.collect.entities.EntitiesRepository -import org.odk.collect.entities.Entity +import org.odk.collect.entities.storage.EntitiesRepository +import org.odk.collect.entities.storage.Entity import org.odk.collect.shared.TempFiles import java.io.File diff --git a/collect_app/src/test/java/org/odk/collect/android/formentry/FormEntryUseCasesTest.kt b/collect_app/src/test/java/org/odk/collect/android/formentry/FormEntryUseCasesTest.kt index 32df90f178b..198e54e219b 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formentry/FormEntryUseCasesTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formentry/FormEntryUseCasesTest.kt @@ -14,7 +14,7 @@ import org.kxml2.kdom.Document import org.mockito.kotlin.mock import org.odk.collect.android.javarosawrapper.FormController import org.odk.collect.android.utilities.FileUtils -import org.odk.collect.entities.InMemEntitiesRepository +import org.odk.collect.entities.storage.InMemEntitiesRepository import org.odk.collect.forms.Form import org.odk.collect.forms.instances.Instance import org.odk.collect.formstest.FormFixtures diff --git a/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java b/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java index a6d19e7d108..86c18791d57 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java @@ -41,7 +41,7 @@ import org.odk.collect.android.tasks.SaveToDiskResult; import org.odk.collect.android.utilities.MediaUtils; import org.odk.collect.audiorecorder.recording.AudioRecorder; -import org.odk.collect.entities.EntitiesRepository; +import org.odk.collect.entities.storage.EntitiesRepository; import org.odk.collect.forms.Form; import org.odk.collect.forms.instances.Instance; import org.odk.collect.forms.instances.InstancesRepository; diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormUseCasesTest.kt b/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormUseCasesTest.kt index 90ae36c5fcc..91240eac0fd 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormUseCasesTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormUseCasesTest.kt @@ -14,7 +14,7 @@ import org.mockito.stubbing.Answer import org.odk.collect.android.formmanagement.download.FormDownloadException import org.odk.collect.android.formmanagement.download.FormDownloader import org.odk.collect.android.utilities.FileUtils -import org.odk.collect.entities.InMemEntitiesRepository +import org.odk.collect.entities.storage.InMemEntitiesRepository import org.odk.collect.forms.Form import org.odk.collect.forms.FormSource import org.odk.collect.forms.ManifestFile diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/download/ServerFormDownloaderTest.java b/collect_app/src/test/java/org/odk/collect/android/formmanagement/download/ServerFormDownloaderTest.java index eb01f7f8fba..5bc4a754fd6 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formmanagement/download/ServerFormDownloaderTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/download/ServerFormDownloaderTest.java @@ -25,8 +25,8 @@ import org.junit.Test; import org.odk.collect.android.formmanagement.FormMetadataParser; import org.odk.collect.android.formmanagement.ServerFormDetails; -import org.odk.collect.entities.EntitiesRepository; -import org.odk.collect.entities.InMemEntitiesRepository; +import org.odk.collect.entities.storage.EntitiesRepository; +import org.odk.collect.entities.storage.InMemEntitiesRepository; import org.odk.collect.forms.Form; import org.odk.collect.forms.FormListItem; import org.odk.collect.forms.FormSource; diff --git a/collect_app/src/test/java/org/odk/collect/android/utilities/StubFormController.kt b/collect_app/src/test/java/org/odk/collect/android/utilities/StubFormController.kt index 06dd75f7418..88748d1fa53 100644 --- a/collect_app/src/test/java/org/odk/collect/android/utilities/StubFormController.kt +++ b/collect_app/src/test/java/org/odk/collect/android/utilities/StubFormController.kt @@ -5,7 +5,6 @@ import org.javarosa.core.model.FormIndex import org.javarosa.core.model.data.IAnswerData import org.javarosa.core.model.instance.TreeReference import org.javarosa.core.services.transport.payload.ByteArrayPayload -import org.javarosa.entities.internal.Entities import org.javarosa.form.api.FormEntryCaption import org.javarosa.form.api.FormEntryPrompt import org.odk.collect.android.exception.JavaRosaException @@ -14,6 +13,7 @@ import org.odk.collect.android.javarosawrapper.FormController import org.odk.collect.android.javarosawrapper.InstanceMetadata import org.odk.collect.android.javarosawrapper.SuccessValidationResult import org.odk.collect.android.javarosawrapper.ValidationResult +import org.odk.collect.entities.javarosa.finalization.EntitiesExtra import java.io.File open class StubFormController : FormController { @@ -156,5 +156,5 @@ open class StubFormController : FormController { override fun getAnswer(treeReference: TreeReference?): IAnswerData? = null - override fun getEntities(): Entities? = null + override fun getEntities(): EntitiesExtra? = null } diff --git a/entities/src/main/AndroidManifest.xml b/entities/src/main/AndroidManifest.xml index 5a97c45de2e..83a679f1884 100644 --- a/entities/src/main/AndroidManifest.xml +++ b/entities/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ diff --git a/entities/src/main/java/org/odk/collect/entities/DaggerSetup.kt b/entities/src/main/java/org/odk/collect/entities/DaggerSetup.kt index 6a6ee1d1e56..6a9ce6fe425 100644 --- a/entities/src/main/java/org/odk/collect/entities/DaggerSetup.kt +++ b/entities/src/main/java/org/odk/collect/entities/DaggerSetup.kt @@ -4,6 +4,8 @@ import dagger.Component import dagger.Module import dagger.Provides import org.odk.collect.async.Scheduler +import org.odk.collect.entities.browser.EntityBrowserActivity +import org.odk.collect.entities.storage.EntitiesRepository import javax.inject.Singleton interface EntitiesDependencyComponentProvider { diff --git a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt index 647728688c5..c2b9de01b68 100644 --- a/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt +++ b/entities/src/main/java/org/odk/collect/entities/LocalEntityUseCases.kt @@ -2,15 +2,18 @@ package org.odk.collect.entities import org.javarosa.core.model.instance.CsvExternalInstance import org.javarosa.core.model.instance.TreeElement -import org.javarosa.entities.EntityAction -import org.javarosa.entities.internal.Entities +import org.odk.collect.entities.browser.EntityItemElement +import org.odk.collect.entities.javarosa.finalization.EntitiesExtra +import org.odk.collect.entities.javarosa.spec.EntityAction +import org.odk.collect.entities.storage.EntitiesRepository +import org.odk.collect.entities.storage.Entity import java.io.File object LocalEntityUseCases { @JvmStatic fun updateLocalEntitiesFromForm( - formEntities: Entities?, + formEntities: EntitiesExtra?, entitiesRepository: EntitiesRepository ) { formEntities?.entities?.forEach { formEntity -> diff --git a/entities/src/main/java/org/odk/collect/entities/EntitiesFragment.kt b/entities/src/main/java/org/odk/collect/entities/browser/EntitiesFragment.kt similarity index 96% rename from entities/src/main/java/org/odk/collect/entities/EntitiesFragment.kt rename to entities/src/main/java/org/odk/collect/entities/browser/EntitiesFragment.kt index f0daa09f365..f6a46dbb2bc 100644 --- a/entities/src/main/java/org/odk/collect/entities/EntitiesFragment.kt +++ b/entities/src/main/java/org/odk/collect/entities/browser/EntitiesFragment.kt @@ -1,4 +1,4 @@ -package org.odk.collect.entities +package org.odk.collect.entities.browser import android.content.Context import android.os.Bundle @@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import org.odk.collect.entities.databinding.ListLayoutBinding +import org.odk.collect.entities.storage.Entity import org.odk.collect.lists.RecyclerViewUtils import org.odk.collect.lists.RecyclerViewUtils.matchParentWidth diff --git a/entities/src/main/java/org/odk/collect/entities/EntitiesViewModel.kt b/entities/src/main/java/org/odk/collect/entities/browser/EntitiesViewModel.kt similarity index 88% rename from entities/src/main/java/org/odk/collect/entities/EntitiesViewModel.kt rename to entities/src/main/java/org/odk/collect/entities/browser/EntitiesViewModel.kt index 104390d6074..14bb01fcea7 100644 --- a/entities/src/main/java/org/odk/collect/entities/EntitiesViewModel.kt +++ b/entities/src/main/java/org/odk/collect/entities/browser/EntitiesViewModel.kt @@ -1,9 +1,11 @@ -package org.odk.collect.entities +package org.odk.collect.entities.browser import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.odk.collect.async.Scheduler +import org.odk.collect.entities.storage.EntitiesRepository +import org.odk.collect.entities.storage.Entity class EntitiesViewModel( private val scheduler: Scheduler, diff --git a/entities/src/main/java/org/odk/collect/entities/EntityBrowserActivity.kt b/entities/src/main/java/org/odk/collect/entities/browser/EntityBrowserActivity.kt similarity index 89% rename from entities/src/main/java/org/odk/collect/entities/EntityBrowserActivity.kt rename to entities/src/main/java/org/odk/collect/entities/browser/EntityBrowserActivity.kt index d3a3f2ae60c..c23a1c15201 100644 --- a/entities/src/main/java/org/odk/collect/entities/EntityBrowserActivity.kt +++ b/entities/src/main/java/org/odk/collect/entities/browser/EntityBrowserActivity.kt @@ -1,4 +1,4 @@ -package org.odk.collect.entities +package org.odk.collect.entities.browser import android.os.Bundle import androidx.appcompat.widget.Toolbar @@ -8,6 +8,9 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupWithNavController import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.async.Scheduler +import org.odk.collect.entities.EntitiesDependencyComponentProvider +import org.odk.collect.entities.R +import org.odk.collect.entities.storage.EntitiesRepository import org.odk.collect.strings.localization.LocalizedActivity import javax.inject.Inject diff --git a/entities/src/main/java/org/odk/collect/entities/EntityItemElement.kt b/entities/src/main/java/org/odk/collect/entities/browser/EntityItemElement.kt similarity index 76% rename from entities/src/main/java/org/odk/collect/entities/EntityItemElement.kt rename to entities/src/main/java/org/odk/collect/entities/browser/EntityItemElement.kt index 9179eb8da10..b55099251b0 100644 --- a/entities/src/main/java/org/odk/collect/entities/EntityItemElement.kt +++ b/entities/src/main/java/org/odk/collect/entities/browser/EntityItemElement.kt @@ -1,4 +1,4 @@ -package org.odk.collect.entities +package org.odk.collect.entities.browser internal object EntityItemElement { const val ID = "name" diff --git a/entities/src/main/java/org/odk/collect/entities/EntityItemView.kt b/entities/src/main/java/org/odk/collect/entities/browser/EntityItemView.kt similarity index 88% rename from entities/src/main/java/org/odk/collect/entities/EntityItemView.kt rename to entities/src/main/java/org/odk/collect/entities/browser/EntityItemView.kt index 3c349347fac..ca0693e1468 100644 --- a/entities/src/main/java/org/odk/collect/entities/EntityItemView.kt +++ b/entities/src/main/java/org/odk/collect/entities/browser/EntityItemView.kt @@ -1,10 +1,11 @@ -package org.odk.collect.entities +package org.odk.collect.entities.browser import android.content.Context import android.view.LayoutInflater import android.widget.FrameLayout import androidx.core.view.isVisible import org.odk.collect.entities.databinding.EntityItemLayoutBinding +import org.odk.collect.entities.storage.Entity class EntityItemView(context: Context) : FrameLayout(context) { diff --git a/entities/src/main/java/org/odk/collect/entities/EntityListsFragment.kt b/entities/src/main/java/org/odk/collect/entities/browser/EntityListsFragment.kt similarity index 98% rename from entities/src/main/java/org/odk/collect/entities/EntityListsFragment.kt rename to entities/src/main/java/org/odk/collect/entities/browser/EntityListsFragment.kt index 6e1470fe508..2b21463c924 100644 --- a/entities/src/main/java/org/odk/collect/entities/EntityListsFragment.kt +++ b/entities/src/main/java/org/odk/collect/entities/browser/EntityListsFragment.kt @@ -1,4 +1,4 @@ -package org.odk.collect.entities +package org.odk.collect.entities.browser import android.content.Context import android.os.Bundle @@ -19,6 +19,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.odk.collect.entities.R import org.odk.collect.entities.databinding.AddEntitiesDialogLayoutBinding import org.odk.collect.entities.databinding.EntityListItemLayoutBinding import org.odk.collect.entities.databinding.ListLayoutBinding diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntitiesExtra.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntitiesExtra.kt new file mode 100644 index 00000000000..877a36ba7d1 --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntitiesExtra.kt @@ -0,0 +1,3 @@ +package org.odk.collect.entities.javarosa.finalization + +data class EntitiesExtra(val entities: List) diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/Entity.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/Entity.kt new file mode 100644 index 00000000000..efd019db78d --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/Entity.kt @@ -0,0 +1,12 @@ +package org.odk.collect.entities.javarosa.finalization + +import org.odk.collect.entities.javarosa.spec.EntityAction + +class Entity( + @JvmField val action: EntityAction, + @JvmField val dataset: String, + @JvmField val id: String?, + @JvmField val label: String?, + @JvmField val version: Int, + @JvmField val properties: List> +) diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.java b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.java new file mode 100644 index 00000000000..ee4a6f4e4f4 --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/finalization/EntityFormFinalizationProcessor.java @@ -0,0 +1,68 @@ +package org.odk.collect.entities.javarosa.finalization; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; + +import org.javarosa.core.model.FormDef; +import org.javarosa.core.model.IDataReference; +import org.javarosa.core.model.data.IAnswerData; +import org.javarosa.core.model.instance.FormInstance; +import org.javarosa.core.model.instance.TreeElement; +import org.javarosa.form.api.FormEntryFinalizationProcessor; +import org.javarosa.form.api.FormEntryModel; +import org.javarosa.model.xform.XPathReference; +import org.odk.collect.entities.javarosa.spec.EntityAction; +import org.odk.collect.entities.javarosa.spec.EntityFormParser; +import org.odk.collect.entities.javarosa.parse.EntityFormExtra; + +import java.util.List; +import java.util.stream.Collectors; + +import kotlin.Pair; + +public class EntityFormFinalizationProcessor implements FormEntryFinalizationProcessor { + + @Override + public void processForm(FormEntryModel formEntryModel) { + FormDef formDef = formEntryModel.getForm(); + FormInstance mainInstance = formDef.getMainInstance(); + + EntityFormExtra entityFormExtra = formDef.getExtras().get(EntityFormExtra.class); + List> saveTos = entityFormExtra.getSaveTos(); + + TreeElement entityElement = EntityFormParser.getEntityElement(mainInstance); + if (entityElement != null) { + EntityAction action = EntityFormParser.parseAction(entityElement); + String dataset = EntityFormParser.parseDataset(entityElement); + + if (action == EntityAction.CREATE) { + Entity entity = createEntity(entityElement, 1, dataset, saveTos, mainInstance, action); + formEntryModel.getExtras().put(new EntitiesExtra(asList(entity))); + } else if (action == EntityAction.UPDATE) { + int baseVersion = EntityFormParser.parseBaseVersion(entityElement); + int newVersion = baseVersion + 1; + Entity entity = createEntity(entityElement, newVersion, dataset, saveTos, mainInstance, action); + formEntryModel.getExtras().put(new EntitiesExtra(asList(entity))); + } else { + formEntryModel.getExtras().put(new EntitiesExtra(emptyList())); + } + } + } + + private Entity createEntity(TreeElement entityElement, int version, String dataset, List> saveTos, FormInstance mainInstance, EntityAction action) { + List> fields = saveTos.stream().map(saveTo -> { + IDataReference reference = saveTo.getFirst(); + IAnswerData answerData = mainInstance.resolveReference(reference).getValue(); + + if (answerData != null) { + return new Pair<>(saveTo.getSecond(), answerData.getDisplayText()); + } else { + return new Pair<>(saveTo.getSecond(), ""); + } + }).collect(Collectors.toList()); + + String id = EntityFormParser.parseId(entityElement); + String label = EntityFormParser.parseLabel(entityElement); + return new Entity(action, dataset, id, label, version, fields); + } +} diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/intance/LocalEntitiesExternalInstanceParserFactory.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/intance/LocalEntitiesExternalInstanceParserFactory.kt new file mode 100644 index 00000000000..3b0ee4a0e81 --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/intance/LocalEntitiesExternalInstanceParserFactory.kt @@ -0,0 +1,20 @@ +package org.odk.collect.entities.javarosa.intance + +import org.javarosa.xform.parse.ExternalInstanceParser +import org.javarosa.xform.parse.ExternalInstanceParserFactory +import org.odk.collect.entities.storage.EntitiesRepository + +class LocalEntitiesExternalInstanceParserFactory( + private val entitiesRepositoryProvider: () -> EntitiesRepository, + private val enabled: () -> Boolean +) : ExternalInstanceParserFactory { + override fun getExternalInstanceParser(): ExternalInstanceParser { + val parser = ExternalInstanceParser() + + if (enabled()) { + parser.addFileInstanceParser(LocalEntitiesFileInstanceParser(entitiesRepositoryProvider)) + } + + return parser + } +} diff --git a/entities/src/main/java/org/odk/collect/entities/LocalEntitiesExternalInstanceParserFactory.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/intance/LocalEntitiesFileInstanceParser.kt similarity index 71% rename from entities/src/main/java/org/odk/collect/entities/LocalEntitiesExternalInstanceParserFactory.kt rename to entities/src/main/java/org/odk/collect/entities/javarosa/intance/LocalEntitiesFileInstanceParser.kt index 09c0430690f..1ec005eb456 100644 --- a/entities/src/main/java/org/odk/collect/entities/LocalEntitiesExternalInstanceParserFactory.kt +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/intance/LocalEntitiesFileInstanceParser.kt @@ -1,28 +1,13 @@ -package org.odk.collect.entities +package org.odk.collect.entities.javarosa.intance import org.javarosa.core.model.data.StringData import org.javarosa.core.model.instance.TreeElement import org.javarosa.xform.parse.ExternalInstanceParser -import org.javarosa.xform.parse.ExternalInstanceParser.FileInstanceParser -import org.javarosa.xform.parse.ExternalInstanceParserFactory - -class LocalEntitiesExternalInstanceParserFactory( - private val entitiesRepositoryProvider: () -> EntitiesRepository, - private val enabled: () -> Boolean -) : ExternalInstanceParserFactory { - override fun getExternalInstanceParser(): ExternalInstanceParser { - val parser = ExternalInstanceParser() - - if (enabled()) { - parser.addFileInstanceParser(LocalEntitiesFileInstanceParser(entitiesRepositoryProvider)) - } - - return parser - } -} +import org.odk.collect.entities.browser.EntityItemElement +import org.odk.collect.entities.storage.EntitiesRepository internal class LocalEntitiesFileInstanceParser(private val entitiesRepositoryProvider: () -> EntitiesRepository) : - FileInstanceParser { + ExternalInstanceParser.FileInstanceParser { override fun parse(instanceId: String, path: String): TreeElement { val root = TreeElement("root", 0) diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityFormExtra.java b/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityFormExtra.java new file mode 100644 index 00000000000..d0cf6d4eb5b --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityFormExtra.java @@ -0,0 +1,47 @@ +package org.odk.collect.entities.javarosa.parse; + +import kotlin.Pair; +import org.javarosa.core.util.externalizable.DeserializationException; +import org.javarosa.core.util.externalizable.ExtUtil; +import org.javarosa.core.util.externalizable.ExtWrapMap; +import org.javarosa.core.util.externalizable.Externalizable; +import org.javarosa.core.util.externalizable.PrototypeFactory; +import org.javarosa.model.xform.XPathReference; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class EntityFormExtra implements Externalizable { + + private List> saveTos = new ArrayList<>(); + + public EntityFormExtra() { + } + + public EntityFormExtra(List> saveTos) { + this.saveTos = saveTos; + } + + public List> getSaveTos() { + return saveTos; + } + + @Override + public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { + HashMap saveToMap = (HashMap) ExtUtil.read(in, new ExtWrapMap(XPathReference.class, String.class), pf); + saveTos = saveToMap.entrySet().stream().map(entry -> new Pair<>(entry.getKey(), entry.getValue())).collect(Collectors.toList()); + } + + @Override + public void writeExternal(DataOutputStream out) throws IOException { + Map saveTosMap = saveTos.stream() + .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond)); + ExtUtil.write(out, new ExtWrapMap(new HashMap<>(saveTosMap))); + } +} diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityFormParseProcessor.java b/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityFormParseProcessor.java new file mode 100644 index 00000000000..9ea85831dad --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityFormParseProcessor.java @@ -0,0 +1,64 @@ +package org.odk.collect.entities.javarosa.parse; + +import kotlin.Pair; +import org.javarosa.core.model.DataBinding; +import org.javarosa.core.model.FormDef; +import org.javarosa.model.xform.XPathReference; +import org.javarosa.xform.parse.XFormParser; +import org.odk.collect.entities.javarosa.spec.EntityFormParser; +import org.odk.collect.entities.javarosa.spec.UnrecognizedEntityVersionException; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +public class EntityFormParseProcessor implements XFormParser.BindAttributeProcessor, XFormParser.FormDefProcessor, XFormParser.ModelAttributeProcessor { + + private static final String ENTITIES_NAMESPACE = "http://www.opendatakit.org/xforms/entities"; + private static final String[] SUPPORTED_VERSIONS = {"2022.1", "2023.1"}; + + private final List> saveTos = new ArrayList<>(); + private boolean versionPresent; + + @Override + public Set> getModelAttributes() { + HashSet> attributes = new HashSet<>(); + attributes.add(new Pair<>(ENTITIES_NAMESPACE, "entities-version")); + + return attributes; + } + + @Override + public void processModelAttribute(String name, String value) throws XFormParser.ParseException { + versionPresent = true; + + if (Stream.of(SUPPORTED_VERSIONS).noneMatch(value::startsWith)) { + throw new UnrecognizedEntityVersionException(); + } + } + + @Override + public Set> getBindAttributes() { + HashSet> attributes = new HashSet<>(); + attributes.add(new Pair<>(ENTITIES_NAMESPACE, "saveto")); + + return attributes; + } + + @Override + public void processBindAttribute(String name, String value, DataBinding binding) { + saveTos.add(new Pair<>((XPathReference) binding.getReference(), value)); + } + + @Override + public void processFormDef(FormDef formDef) throws XFormParser.ParseException { + if (!versionPresent && EntityFormParser.getEntityElement(formDef.getMainInstance()) != null) { + throw new XFormParser.MissingModelAttributeException(ENTITIES_NAMESPACE, "entities-version"); + } + + EntityFormExtra entityFormExtra = new EntityFormExtra(saveTos); + formDef.getExtras().put(entityFormExtra); + } +} diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityXFormParserFactory.java b/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityXFormParserFactory.java new file mode 100644 index 00000000000..6e735509e00 --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityXFormParserFactory.java @@ -0,0 +1,20 @@ +package org.odk.collect.entities.javarosa.parse; + +import org.javarosa.xform.parse.IXFormParserFactory; +import org.javarosa.xform.parse.XFormParser; +import org.jetbrains.annotations.NotNull; + +public class EntityXFormParserFactory extends IXFormParserFactory.Wrapper { + + public EntityXFormParserFactory(IXFormParserFactory base) { + super(base); + } + + @Override + public XFormParser apply(@NotNull XFormParser parser) { + EntityFormParseProcessor processor = new EntityFormParseProcessor(); + parser.addProcessor(processor); + + return parser; + } +} diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/spec/EntityAction.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/spec/EntityAction.kt new file mode 100644 index 00000000000..70995d8c1f6 --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/spec/EntityAction.kt @@ -0,0 +1,6 @@ +package org.odk.collect.entities.javarosa.spec + +enum class EntityAction { + CREATE, + UPDATE +} diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/spec/EntityConstants.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/spec/EntityConstants.kt new file mode 100644 index 00000000000..5ac564bd022 --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/spec/EntityConstants.kt @@ -0,0 +1,12 @@ +package org.odk.collect.entities.javarosa.spec + +internal object EntityConstants { + const val ELEMENT_ENTITY = "entity" + const val ELEMENT_LABEL = "label" + + const val ATTRIBUTE_DATASET = "dataset" + const val ATTRIBUTE_ID = "id" + const val ATTRIBUTE_BASE_VERSION = "baseVersion" + const val ATTRIBUTE_CREATE = "create" + const val ATTRIBUTE_UPDATE = "update" +} diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/spec/EntityFormParser.java b/entities/src/main/java/org/odk/collect/entities/javarosa/spec/EntityFormParser.java new file mode 100644 index 00000000000..fa564752622 --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/spec/EntityFormParser.java @@ -0,0 +1,89 @@ +package org.odk.collect.entities.javarosa.spec; + +import org.javarosa.core.model.data.IAnswerData; +import org.javarosa.core.model.instance.FormInstance; +import org.javarosa.core.model.instance.TreeElement; +import org.javarosa.xpath.expr.XPathFuncExpr; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.odk.collect.entities.javarosa.spec.EntityConstants.ATTRIBUTE_BASE_VERSION; +import static org.odk.collect.entities.javarosa.spec.EntityConstants.ATTRIBUTE_CREATE; +import static org.odk.collect.entities.javarosa.spec.EntityConstants.ATTRIBUTE_DATASET; +import static org.odk.collect.entities.javarosa.spec.EntityConstants.ATTRIBUTE_ID; +import static org.odk.collect.entities.javarosa.spec.EntityConstants.ATTRIBUTE_UPDATE; +import static org.odk.collect.entities.javarosa.spec.EntityConstants.ELEMENT_ENTITY; +import static org.odk.collect.entities.javarosa.spec.EntityConstants.ELEMENT_LABEL; + +public class EntityFormParser { + + private EntityFormParser() { + + } + + public static String parseDataset(TreeElement entity) { + return entity.getAttributeValue(null, ATTRIBUTE_DATASET); + } + + @Nullable + public static String parseLabel(TreeElement entity) { + TreeElement labelElement = entity.getFirstChild(ELEMENT_LABEL); + + if (labelElement != null) { + IAnswerData labelValue = labelElement.getValue(); + + if (labelValue != null) { + return String.valueOf(labelValue.getValue()); + } else { + return null; + } + } else { + return null; + } + } + + @Nullable + public static String parseId(TreeElement entity) { + return entity.getAttributeValue("", ATTRIBUTE_ID); + } + + public static Integer parseBaseVersion(TreeElement entity) { + try { + return Integer.valueOf(entity.getAttributeValue("", ATTRIBUTE_BASE_VERSION)); + } catch (NumberFormatException e) { + return 0; + } + } + + @Nullable + public static TreeElement getEntityElement(FormInstance mainInstance) { + TreeElement root = mainInstance.getRoot(); + TreeElement meta = root.getFirstChild("meta"); + + if (meta != null) { + return meta.getFirstChild(ELEMENT_ENTITY); + } else { + return null; + } + } + + @Nullable + public static EntityAction parseAction(@NotNull TreeElement entity) { + String create = entity.getAttributeValue(null, ATTRIBUTE_CREATE); + String update = entity.getAttributeValue(null, ATTRIBUTE_UPDATE); + + if (update != null) { + if (XPathFuncExpr.boolStr(update)) { + return EntityAction.UPDATE; + } + } + + if (create != null) { + if (XPathFuncExpr.boolStr(create)) { + return EntityAction.CREATE; + } + } + + return null; + } +} diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/spec/UnrecognizedEntityVersionException.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/spec/UnrecognizedEntityVersionException.kt new file mode 100644 index 00000000000..e064ad9328a --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/spec/UnrecognizedEntityVersionException.kt @@ -0,0 +1,5 @@ +package org.odk.collect.entities.javarosa.spec + +import org.javarosa.xform.parse.XFormParser + +class UnrecognizedEntityVersionException : XFormParser.ParseException() diff --git a/entities/src/main/java/org/odk/collect/entities/EntitiesRepository.kt b/entities/src/main/java/org/odk/collect/entities/storage/EntitiesRepository.kt similarity index 84% rename from entities/src/main/java/org/odk/collect/entities/EntitiesRepository.kt rename to entities/src/main/java/org/odk/collect/entities/storage/EntitiesRepository.kt index c72ae786503..9794f5dfdde 100644 --- a/entities/src/main/java/org/odk/collect/entities/EntitiesRepository.kt +++ b/entities/src/main/java/org/odk/collect/entities/storage/EntitiesRepository.kt @@ -1,4 +1,4 @@ -package org.odk.collect.entities +package org.odk.collect.entities.storage interface EntitiesRepository { fun save(vararg entities: Entity) diff --git a/entities/src/main/java/org/odk/collect/entities/Entity.kt b/entities/src/main/java/org/odk/collect/entities/storage/Entity.kt similarity index 88% rename from entities/src/main/java/org/odk/collect/entities/Entity.kt rename to entities/src/main/java/org/odk/collect/entities/storage/Entity.kt index 3f812fb7ed9..77f7370ac3c 100644 --- a/entities/src/main/java/org/odk/collect/entities/Entity.kt +++ b/entities/src/main/java/org/odk/collect/entities/storage/Entity.kt @@ -1,4 +1,4 @@ -package org.odk.collect.entities +package org.odk.collect.entities.storage data class Entity @JvmOverloads constructor( val list: String, diff --git a/entities/src/main/java/org/odk/collect/entities/InMemEntitiesRepository.kt b/entities/src/main/java/org/odk/collect/entities/storage/InMemEntitiesRepository.kt similarity index 97% rename from entities/src/main/java/org/odk/collect/entities/InMemEntitiesRepository.kt rename to entities/src/main/java/org/odk/collect/entities/storage/InMemEntitiesRepository.kt index 6b1127bded9..952bf51d33f 100644 --- a/entities/src/main/java/org/odk/collect/entities/InMemEntitiesRepository.kt +++ b/entities/src/main/java/org/odk/collect/entities/storage/InMemEntitiesRepository.kt @@ -1,4 +1,4 @@ -package org.odk.collect.entities +package org.odk.collect.entities.storage class InMemEntitiesRepository : EntitiesRepository { diff --git a/entities/src/main/res/navigation/entities_nav.xml b/entities/src/main/res/navigation/entities_nav.xml index 6bb20869435..646bf21a7ac 100644 --- a/entities/src/main/res/navigation/entities_nav.xml +++ b/entities/src/main/res/navigation/entities_nav.xml @@ -6,7 +6,7 @@ ("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Entity form"), + model(asList(new Pair<>("entities:entities-version", "2022.1.1")), + mainInstance( + t("data id=\"entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\"") + ) + ) + ), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name") + ) + ), + body( + input("/data/name") + ) + )); + + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + + scenario.next(); + scenario.answer("Tom Wambsgans"); + + scenario.finalizeInstance(); + List entities = scenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(0)); + } + + @Test + public void fillingFormWithCreate_makesEntityAvailable() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("Create entity form", XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Create entity form"), + model(asList(new Pair<>("entities:entities-version", "2022.1.1")), + mainInstance( + t("data id=\"create-entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\" create=\"1\" id=\"\"", + t("label") + ) + ) + ) + ), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name"), + bind("/data/meta/entity/@id").type("string"), + bind("/data/meta/entity/label").type("string").calculate("/data/name"), + setvalue("odk-instance-first-load", "/data/meta/entity/@id", "uuid()") + ) + ), + body( + input("/data/name") + ) + )); + + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + + scenario.next(); + scenario.answer("Tom Wambsgans"); + + scenario.finalizeInstance(); + List entities = scenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(1)); + assertThat(entities.get(0).dataset, equalTo("people")); + assertThat(entities.get(0).id, equalTo(scenario.answerOf("/data/meta/entity/@id").getValue())); + assertThat(entities.get(0).label, equalTo("Tom Wambsgans")); + assertThat(entities.get(0).version, equalTo(1)); + assertThat(entities.get(0).properties, equalTo(asList(new Pair<>("name", "Tom Wambsgans")))); + assertThat(entities.get(0).action, equalTo(EntityAction.CREATE)); + } + + @Test + public void fillingFormWithCreate_withoutAnId_makesEntityAvailable() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("Create entity form", XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Create entity form"), + model(asList(new Pair<>("entities:entities-version", "2022.1.1")), + mainInstance( + t("data id=\"create-entity-form\"", + t("id"), + t("name"), + t("meta", + t("entity dataset=\"people\" create=\"1\" id=\"\"", + t("label") + ) + ) + ) + ), + bind("/data/id").type("string"), + bind("/data/meta/entity/@id").type("string").calculate("/data/id"), + bind("/data/meta/entity/label").type("string").calculate("/data/name") + ) + ), + body( + input("/data/id"), + input("/data/name") + ) + )); + + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + scenario.finalizeInstance(); + + List entities = scenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(1)); + assertThat(entities.get(0).dataset, equalTo("people")); + assertThat(entities.get(0).id, equalTo(null)); + assertThat(entities.get(0).action, equalTo(EntityAction.CREATE)); + } + + @Test + public void fillingFormWithUpdate_makesEntityAvailable() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("Create entity form", XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Update entity form"), + model(asList(new Pair<>("entities:entities-version", "2023.1.0")), + mainInstance( + t("data id=\"update-entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\" update=\"1\" id=\"123\" baseVersion=\"1\"", + t("label") + ) + ) + ) + ), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name"), + bind("/data/meta/entity/@id").type("string"), + bind("/data/meta/entity/label").type("string").calculate("/data/name") + ) + ), + body( + input("/data/name") + ) + )); + + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + + scenario.next(); + scenario.answer("Tom Wambsgans"); + + scenario.finalizeInstance(); + List entities = scenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(1)); + assertThat(entities.get(0).dataset, equalTo("people")); + assertThat(entities.get(0).id, equalTo("123")); + assertThat(entities.get(0).label, equalTo("Tom Wambsgans")); + assertThat(entities.get(0).version, equalTo(2)); + assertThat(entities.get(0).properties, equalTo(asList(new Pair<>("name", "Tom Wambsgans")))); + assertThat(entities.get(0).action, equalTo(EntityAction.UPDATE)); + } + + @Test + public void fillingFormWithUpdate_andNoLabel_makesEntityAvailableWithNullLabel() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("Update entity form", XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Update entity form"), + model(asList(new Pair<>("entities:entities-version", "2023.1.0")), + mainInstance( + t("data id=\"update-entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\" update=\"1\" id=\"123\" baseVersion=\"1\"") + ) + ) + ), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name"), + bind("/data/meta/entity/@id").type("string") + ) + ), + body( + input("/data/name") + ) + )); + + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + + scenario.next(); + scenario.answer("Tom Wambsgans"); + + scenario.finalizeInstance(); + List entities = scenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(1)); + assertThat(entities.get(0).dataset, equalTo("people")); + assertThat(entities.get(0).id, equalTo("123")); + assertThat(entities.get(0).label, equalTo(null)); + assertThat(entities.get(0).version, equalTo(2)); + assertThat(entities.get(0).properties, equalTo(asList(new Pair<>("name", "Tom Wambsgans")))); + } + + @Test + public void fillingFormWithUpdate_withNullId_makesEntityAvailable() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("Create entity form", XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Update entity form"), + model(asList(new Pair<>("entities:entities-version", "2023.1.0")), + mainInstance( + t("data id=\"update-entity-form\"", + t("id"), + t("meta", + t("entity dataset=\"people\" update=\"1\" id=\"\" baseVersion=\"\"") + ) + ) + ), + bind("/data/id").type("string"), + bind("/data/meta/entity/@id").type("string").calculate("/data/id").readonly() + ) + ), + body( + input("/data/id") + ) + )); + + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + scenario.finalizeInstance(); + + List entities = scenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(1)); + assertThat(entities.get(0).dataset, equalTo("people")); + assertThat(entities.get(0).id, equalTo(null)); + assertThat(entities.get(0).action, equalTo(EntityAction.UPDATE)); + } + + @Test + public void fillingFormWithCreateAndUpdate_makesEntityAvailableAsSecondVersion() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("Create entity form", XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Upsert entity form"), + model(asList(new Pair<>("entities:entities-version", "2023.1.0")), + mainInstance( + t("data id=\"upsert-entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\" create=\"1\" update=\"1\" id=\"123\" baseVersion=\"1\"", + t("label") + ) + ) + ) + ), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name"), + bind("/data/meta/entity/label").type("string").calculate("/data/name") + ) + ), + body( + input("/data/name") + ) + )); + + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + + scenario.next(); + scenario.answer("Tom Wambsgans"); + + scenario.finalizeInstance(); + List entities = scenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(1)); + assertThat(entities.get(0).dataset, equalTo("people")); + assertThat(entities.get(0).id, equalTo("123")); + assertThat(entities.get(0).label, equalTo("Tom Wambsgans")); + assertThat(entities.get(0).version, equalTo(2)); + assertThat(entities.get(0).properties, equalTo(asList(new Pair<>("name", "Tom Wambsgans")))); + assertThat(entities.get(0).action, equalTo(EntityAction.UPDATE)); + } + + @Test + public void fillingFormWithCreateAndUpdate_butNoBaseVersion_makesEntityAvailableAsFirstVersion() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("Create entity form", XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Upsert entity form"), + model(asList(new Pair<>("entities:entities-version", "2023.1.0")), + mainInstance( + t("data id=\"upsert-entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\" create=\"1\" update=\"1\" id=\"123\"", + t("label") + ) + ) + ) + ), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name"), + bind("/data/meta/entity/label").type("string").calculate("/data/name") + ) + ), + body( + input("/data/name") + ) + )); + + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + + scenario.next(); + scenario.answer("Tom Wambsgans"); + + scenario.finalizeInstance(); + List entities = scenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(1)); + assertThat(entities.get(0).dataset, equalTo("people")); + assertThat(entities.get(0).id, equalTo("123")); + assertThat(entities.get(0).label, equalTo("Tom Wambsgans")); + assertThat(entities.get(0).version, equalTo(1)); + assertThat(entities.get(0).properties, equalTo(asList(new Pair<>("name", "Tom Wambsgans")))); + assertThat(entities.get(0).action, equalTo(EntityAction.UPDATE)); + } + + @Test + public void fillingFormWithDynamicCreateExpression_conditionallyCreatesEntities() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("Create entity form", XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Create entity form"), + model(asList(new Pair<>("entities:entities-version", "2022.1.1")), + mainInstance( + t("data id=\"create-entity-form\"", + t("name"), + t("join"), + t("meta", + t("entity dataset=\"members\" create=\"\" id=\"1\"") + ) + ) + ), + bind("/data/meta/entity/@create").calculate("/data/join = 'yes'"), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name") + ) + ), + body( + input("/data/name"), + select1("/data/join", item("yes", "Yes"), item("no", "No")) + ) + )); + + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + + scenario.next(); + scenario.answer("Roman Roy"); + scenario.next(); + scenario.answer(scenario.choicesOf("/data/join").get(0)); + + scenario.finalizeInstance(); + List entities = scenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(1)); + + scenario.newInstance(); + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + scenario.next(); + scenario.answer("Roman Roy"); + scenario.next(); + scenario.answer(scenario.choicesOf("/data/join").get(1)); + + scenario.finalizeInstance(); + entities = scenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(0)); + } + + @Test + public void entityFormCanBeSerialized() throws IOException, DeserializationException, XFormParser.ParseException { + Scenario scenario = Scenario.init("Create entity form", XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Create entity form"), + model(asList(new Pair<>("entities:entities-version", "2022.1.1")), + mainInstance( + t("data id=\"create-entity-form\"", + t("name"), + t("meta", + t("entities:entity dataset=\"people\" create=\"1\" id=\"1\"") + ) + ) + ), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name") + ) + ), + body( + input("/data/name") + ) + )); + + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + + Scenario deserializedScenario = scenario.serializeAndDeserializeForm(); + deserializedScenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + + deserializedScenario.next(); + deserializedScenario.answer("Shiv Roy"); + + deserializedScenario.finalizeInstance(); + List entities = deserializedScenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(1)); + assertThat(entities.get(0).dataset, equalTo("people")); + assertThat(entities.get(0).properties, equalTo(asList(new Pair<>("name", "Shiv Roy")))); + } + + @Test + public void entitiesNamespaceWorksRegardlessOfName() throws IOException, DeserializationException, XFormParser.ParseException { + Scenario scenario = Scenario.init("Create entity form", XFormsElement.html( + asList( + new Pair<>("blah", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Create entity form"), + model(asList(new Pair<>("blah:entities-version", "2022.1.1")), + mainInstance( + t("data id=\"create-entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\" create=\"1\" id=\"1\"") + ) + ) + ), + bind("/data/name").type("string").withAttribute("blah", "saveto", "name") + ) + ), + body( + input("/data/name") + ) + )); + + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + + scenario.next(); + scenario.answer("Tom Wambsgans"); + + scenario.finalizeInstance(); + List entities = scenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(1)); + assertThat(entities.get(0).properties, equalTo(asList(new Pair<>("name", "Tom Wambsgans")))); + } + + @Test + public void fillingFormWithSelectSaveTo_andWithCreate_savesValuesCorrectlyToEntity() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("Create entity form", XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Create entity form"), + model(asList(new Pair<>("entities:entities-version", "2022.1.1")), + mainInstance( + t("data id=\"create-entity-form\"", + t("team"), + t("meta", + t("entity dataset=\"people\" create=\"1\" id=\"1\"") + ) + ) + ), + bind("/data/team").type("string").withAttribute("entities", "saveto", "team") + ) + ), + body( + select1("/data/team", item("kendall", "Kendall"), item("logan", "Logan")) + ) + )); + + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + + scenario.next(); + scenario.answer(scenario.choicesOf("/data/team").get(0)); + + scenario.finalizeInstance(); + List entities = scenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(1)); + assertThat(entities.get(0).properties, equalTo(asList(new Pair<>("team", "kendall")))); + } + + @Test + public void whenSaveToQuestionIsNotAnswered_entityPropertyIsEmptyString() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("Create entity form", XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Create entity form"), + model(asList(new Pair<>("entities:entities-version", "2022.1.1")), + mainInstance( + t("data id=\"create-entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\" create=\"1\" id=\"1\"") + ) + ) + ), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name") + ) + ), + body( + input("/data/name") + ) + )); + + scenario.getFormEntryController().addPostProcessor(new EntityFormFinalizationProcessor()); + scenario.finalizeInstance(); + + List entities = scenario.getFormEntryController().getModel().getExtras().get(EntitiesExtra.class).getEntities(); + assertThat(entities.size(), equalTo(1)); + assertThat(entities.get(0).properties, equalTo(asList(new Pair<>("name", "")))); + } + + @Test + public void savetoIsRemovedFromBindAttributesForClients() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("Create entity form", XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Create entity form"), + model(asList(new Pair<>("entities:entities-version", "2022.1.1")), + mainInstance( + t("data id=\"create-entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\" create=\"1\"") + ) + ) + ), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name") + ) + ), + body( + input("/data/name") + ) + )); + + scenario.next(); + List bindAttributes = scenario.getFormEntryPromptAtIndex().getBindAttributes(); + boolean containsSaveTo = bindAttributes.stream().anyMatch(treeElement -> treeElement.getName().equals("saveto")); + assertThat(containsSaveTo, is(false)); + } +} diff --git a/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.java b/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.java new file mode 100644 index 00000000000..5fc454deeb6 --- /dev/null +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.java @@ -0,0 +1,64 @@ +package org.odk.collect.entities.javarosa; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.javarosa.test.BindBuilderXFormsElement.bind; +import static org.javarosa.test.XFormsElement.body; +import static org.javarosa.test.XFormsElement.head; +import static org.javarosa.test.XFormsElement.input; +import static org.javarosa.test.XFormsElement.mainInstance; +import static org.javarosa.test.XFormsElement.model; +import static org.javarosa.test.XFormsElement.t; +import static org.javarosa.test.XFormsElement.title; + +import org.javarosa.form.api.FormEntryModel; +import org.javarosa.test.Scenario; +import org.javarosa.test.XFormsElement; +import org.javarosa.xform.parse.XFormParserFactory; +import org.javarosa.xform.util.XFormUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.odk.collect.entities.javarosa.finalization.EntitiesExtra; +import org.odk.collect.entities.javarosa.finalization.EntityFormFinalizationProcessor; +import org.odk.collect.entities.javarosa.parse.EntityXFormParserFactory; + +public class EntityFormFinalizationProcessorTest { + + private final EntityXFormParserFactory entityXFormParserFactory = new EntityXFormParserFactory(new XFormParserFactory()); + + @Before + public void setup() { + XFormUtils.setXFormParserFactory(entityXFormParserFactory); + } + + @After + public void teardown() { + XFormUtils.setXFormParserFactory(new XFormParserFactory()); + } + + @Test + public void whenFormDoesNotHaveEntityElement_addsNoEntitiesToExtras() throws Exception { + Scenario scenario = Scenario.init("Normal form", XFormsElement.html( + head( + title("Normal form"), + model( + mainInstance( + t("data id=\"normal\"", + t("name") + ) + ), + bind("/data/name").type("string") + ) + ), + body( + input("/data/name") + ) + )); + + EntityFormFinalizationProcessor processor = new EntityFormFinalizationProcessor(); + FormEntryModel model = scenario.getFormEntryController().getModel(); + processor.processForm(model); + assertThat(model.getExtras().get(EntitiesExtra.class), equalTo(null)); + } +} diff --git a/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormParseProcessorTest.java b/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormParseProcessorTest.java new file mode 100644 index 00000000000..983668f6a0e --- /dev/null +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormParseProcessorTest.java @@ -0,0 +1,237 @@ +package org.odk.collect.entities.javarosa; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.javarosa.test.BindBuilderXFormsElement.bind; +import static org.javarosa.test.XFormsElement.body; +import static org.javarosa.test.XFormsElement.head; +import static org.javarosa.test.XFormsElement.input; +import static org.javarosa.test.XFormsElement.mainInstance; +import static org.javarosa.test.XFormsElement.model; +import static org.javarosa.test.XFormsElement.t; +import static org.javarosa.test.XFormsElement.title; +import static org.junit.Assert.fail; +import static java.util.Arrays.asList; + +import org.javarosa.core.model.FormDef; +import org.javarosa.test.XFormsElement; +import org.javarosa.xform.parse.XFormParser; +import org.javarosa.xform.parse.XFormParser.MissingModelAttributeException; +import org.junit.Test; +import org.odk.collect.entities.javarosa.parse.EntityFormExtra; +import org.odk.collect.entities.javarosa.parse.EntityFormParseProcessor; +import org.odk.collect.entities.javarosa.spec.UnrecognizedEntityVersionException; + +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; + +import kotlin.Pair; + +public class EntityFormParseProcessorTest { + + @Test + public void whenVersionIsMissing_parsesWithoutError() throws XFormParser.ParseException { + XFormsElement form = XFormsElement.html( + head( + title("Non entity form"), + model( + mainInstance( + t("data id=\"create-entity-form\"", + t("name"), + t("meta") + ) + ), + bind("/data/name").type("string") + ) + ), + body( + input("/data/name") + ) + ); + + EntityFormParseProcessor processor = new EntityFormParseProcessor(); + XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); + parser.addProcessor(processor); + parser.parse(null); + } + + @Test + public void whenVersionIsMissing_andThereIsAnEntityElement_throwsException() { + XFormsElement form = XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Create entity form"), + model( + mainInstance( + t("data id=\"create-entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\"") + ) + ) + ), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name") + ) + ), + body( + input("/data/name") + ) + ); + + EntityFormParseProcessor processor = new EntityFormParseProcessor(); + XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); + parser.addProcessor(processor); + + try { + parser.parse(null); + fail("Expected exception!"); + } catch (Exception e) { + assertThat(e, instanceOf(MissingModelAttributeException.class)); + + MissingModelAttributeException missingModelAttributeException = (MissingModelAttributeException) e; + assertThat(missingModelAttributeException.getNamespace(), equalTo("http://www.opendatakit.org/xforms/entities")); + assertThat(missingModelAttributeException.getName(), equalTo("entities-version")); + } + } + + @Test(expected = UnrecognizedEntityVersionException.class) + public void whenVersionIsNotRecognized_throwsException() throws XFormParser.ParseException { + XFormsElement form = XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Create entity form"), + model(asList(new Pair<>("entities:entities-version", "somethingElse")), + mainInstance( + t("data id=\"create-entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\"") + ) + ) + ), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name") + ) + ), + body( + input("/data/name") + ) + ); + + EntityFormParseProcessor processor = new EntityFormParseProcessor(); + XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); + parser.addProcessor(processor); + parser.parse(null); + } + + @Test + public void whenVersionIsNewPatch_parsesCorrectly() throws XFormParser.ParseException { + String newPatchVersion = "2022.1.12"; + + XFormsElement form = XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Create entity form"), + model(asList(new Pair<>("entities:entities-version", newPatchVersion)), + mainInstance( + t("data id=\"create-entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\"") + ) + ) + ), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name") + ) + ), + body( + input("/data/name") + ) + ); + + EntityFormParseProcessor processor = new EntityFormParseProcessor(); + XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); + parser.addProcessor(processor); + + FormDef formDef = parser.parse(null); + assertThat(formDef.getExtras().get(EntityFormExtra.class), notNullValue()); + } + + @Test + public void whenVersionIsNewVersionWithUpdates_parsesCorrectly() throws XFormParser.ParseException { + String updateVersion = "2023.1.0"; + + XFormsElement form = XFormsElement.html( + asList( + new Pair<>("entities", "http://www.opendatakit.org/xforms/entities") + ), + head( + title("Create entity form"), + model(asList(new Pair<>("entities:entities-version", updateVersion)), + mainInstance( + t("data id=\"update-entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\" update=\"1\" id=\"17\"") + ) + ) + ), + bind("/data/name").type("string").withAttribute("entities", "saveto", "name") + ) + ), + body( + input("/data/name") + ) + ); + + EntityFormParseProcessor processor = new EntityFormParseProcessor(); + XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); + parser.addProcessor(processor); + + FormDef formDef = parser.parse(null); + assertThat(formDef.getExtras().get(EntityFormExtra.class), notNullValue()); + } + + @Test + public void saveTosWithIncorrectNamespaceAreIgnored() throws XFormParser.ParseException { + XFormsElement form = XFormsElement.html( + asList( + new Pair<>("correct", "http://www.opendatakit.org/xforms/entities"), + new Pair<>("incorrect", "blah") + ), + head( + title("Create entity form"), + model(asList(new Pair<>("correct:entities-version", "2022.1.1")), + mainInstance( + t("data id=\"create-entity-form\"", + t("name"), + t("meta", + t("entity dataset=\"people\"") + ) + ) + ), + bind("/data/name").type("string").withAttribute("incorrect", "saveto", "name") + ) + ), + body( + input("/data/name") + ) + ); + + EntityFormParseProcessor processor = new EntityFormParseProcessor(); + XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); + parser.addProcessor(processor); + + FormDef formDef = parser.parse(null); + assertThat(formDef.getExtras().get(EntityFormExtra.class).getSaveTos(), is(empty())); + } +} diff --git a/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormParserTest.java b/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormParserTest.java new file mode 100644 index 00000000000..9f0eea6211a --- /dev/null +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormParserTest.java @@ -0,0 +1,46 @@ +package org.odk.collect.entities.javarosa; + +import org.javarosa.core.model.data.IntegerData; +import org.javarosa.core.model.instance.TreeElement; +import org.junit.Test; +import org.odk.collect.entities.javarosa.spec.EntityAction; +import org.odk.collect.entities.javarosa.spec.EntityFormParser; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.odk.collect.entities.javarosa.spec.EntityConstants.ATTRIBUTE_CREATE; +import static org.odk.collect.entities.javarosa.spec.EntityConstants.ATTRIBUTE_UPDATE; +import static org.odk.collect.entities.javarosa.spec.EntityConstants.ELEMENT_ENTITY; +import static org.odk.collect.entities.javarosa.spec.EntityConstants.ELEMENT_LABEL; + +public class EntityFormParserTest { + + @Test + public void parseAction_findsCreateWithTrueString() { + TreeElement entityElement = new TreeElement(ELEMENT_ENTITY); + entityElement.setAttribute(null, ATTRIBUTE_CREATE, "true"); + + EntityAction action = EntityFormParser.parseAction(entityElement); + assertThat(action, equalTo(EntityAction.CREATE)); + } + + @Test + public void parseAction_findsUpdateWithTrueString() { + TreeElement entityElement = new TreeElement(ELEMENT_ENTITY); + entityElement.setAttribute(null, ATTRIBUTE_UPDATE, "true"); + + EntityAction dataset = EntityFormParser.parseAction(entityElement); + assertThat(dataset, equalTo(EntityAction.UPDATE)); + } + + @Test + public void parseLabel_whenLabelIsAnInt_convertsToString() { + TreeElement labelElement = new TreeElement(ELEMENT_LABEL); + labelElement.setAnswer(new IntegerData(0)); + TreeElement entityElement = new TreeElement(ELEMENT_ENTITY); + entityElement.addChild(labelElement); + + String label = EntityFormParser.parseLabel(entityElement); + assertThat(label, equalTo("0")); + } +} diff --git a/entities/src/test/java/org/odk/collect/entities/LocalEntitiesFileInstanceParserTest.kt b/entities/src/test/java/org/odk/collect/entities/javarosa/LocalEntitiesFileInstanceParserTest.kt similarity index 84% rename from entities/src/test/java/org/odk/collect/entities/LocalEntitiesFileInstanceParserTest.kt rename to entities/src/test/java/org/odk/collect/entities/javarosa/LocalEntitiesFileInstanceParserTest.kt index 8d3a57d0349..9d1184032eb 100644 --- a/entities/src/test/java/org/odk/collect/entities/LocalEntitiesFileInstanceParserTest.kt +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/LocalEntitiesFileInstanceParserTest.kt @@ -1,8 +1,12 @@ -package org.odk.collect.entities +package org.odk.collect.entities.javarosa import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat import org.junit.Test +import org.odk.collect.entities.browser.EntityItemElement +import org.odk.collect.entities.javarosa.intance.LocalEntitiesFileInstanceParser +import org.odk.collect.entities.storage.Entity +import org.odk.collect.entities.storage.InMemEntitiesRepository class LocalEntitiesFileInstanceParserTest {