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 {