From 2924e571664a0bd56e90534c21bb4ce48cfa46ea Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 13 Oct 2025 00:01:54 +0200 Subject: [PATCH 1/5] Convert EntityXFormParserFactory to kotlin --- .../parse/EntityXFormParserFactory.java | 20 ------------------- .../parse/EntityXFormParserFactory.kt | 13 ++++++++++++ 2 files changed, 13 insertions(+), 20 deletions(-) delete mode 100644 entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityXFormParserFactory.java create mode 100644 entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityXFormParserFactory.kt 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 deleted file mode 100644 index 6e735509e00..00000000000 --- a/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityXFormParserFactory.java +++ /dev/null @@ -1,20 +0,0 @@ -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/parse/EntityXFormParserFactory.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityXFormParserFactory.kt new file mode 100644 index 00000000000..a24ad003a75 --- /dev/null +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityXFormParserFactory.kt @@ -0,0 +1,13 @@ +package org.odk.collect.entities.javarosa.parse + +import org.javarosa.xform.parse.IXFormParserFactory +import org.javarosa.xform.parse.XFormParser + +class EntityXFormParserFactory(base: IXFormParserFactory) : IXFormParserFactory.Wrapper(base) { + override fun apply(parser: XFormParser): XFormParser { + val processor = EntityFormParseProcessor() + parser.addProcessor(processor) + + return parser + } +} From 1d4122b744b164cf0a9cf479e365c267a2b940d7 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Mon, 13 Oct 2025 00:42:24 +0200 Subject: [PATCH 2/5] Add experimental opt-in for entities spec v2025.1 --- .../application/initialization/JavaRosaInitializer.kt | 3 ++- .../java/org/odk/collect/android/preferences/Defaults.kt | 1 + collect_app/src/main/res/xml/experimental_preferences.xml | 5 +++++ .../entities/javarosa/parse/EntityFormParseProcessor.kt | 7 ++++--- .../entities/javarosa/parse/EntityXFormParserFactory.kt | 7 +++++-- .../main/java/org/odk/collect/settings/keys/ProjectKeys.kt | 1 + strings/src/main/res/values/strings.xml | 3 ++- 7 files changed, 20 insertions(+), 7 deletions(-) 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 0fd0346311f..63143404a9d 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 @@ -16,6 +16,7 @@ import org.odk.collect.entities.storage.EntitiesRepository import org.odk.collect.metadata.PropertyManager import org.odk.collect.projects.ProjectDependencyFactory import org.odk.collect.settings.SettingsProvider +import org.odk.collect.settings.keys.ProjectKeys class JavaRosaInitializer( private val propertyManager: PropertyManager, @@ -45,7 +46,7 @@ class JavaRosaInitializer( val entityXFormParserFactory = EntityXFormParserFactory( XFormParserFactory() - ) + ) { settingsProvider.getUnprotectedSettings().getBoolean(ProjectKeys.KEY_ENTITIES_SPEC_V2025_1) } val dynamicPreloadXFormParserFactory = DynamicPreloadXFormParserFactory(entityXFormParserFactory) diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/Defaults.kt b/collect_app/src/main/java/org/odk/collect/android/preferences/Defaults.kt index 4995f04e50c..eed87d48bab 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/Defaults.kt +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/Defaults.kt @@ -54,6 +54,7 @@ object Defaults { // experimental_preferences.xml hashMap[ProjectKeys.KEY_DEBUG_FILTERS] = BuildConfig.BUILD_TYPE == "selfSignedRelease" hashMap[ProjectKeys.KEY_ZXING_SCANNING] = false + hashMap[ProjectKeys.KEY_ENTITIES_SPEC_V2025_1] = false return hashMap } diff --git a/collect_app/src/main/res/xml/experimental_preferences.xml b/collect_app/src/main/res/xml/experimental_preferences.xml index 5acf249a54f..9d77e85b0b6 100644 --- a/collect_app/src/main/res/xml/experimental_preferences.xml +++ b/collect_app/src/main/res/xml/experimental_preferences.xml @@ -14,6 +14,11 @@ app:allowDividerBelow="false" app:iconSpaceReserved="false"> + + Boolean +) : BindAttributeProcessor, FormDefProcessor, ModelAttributeProcessor { private val saveTos = mutableListOf>() private var version: String? = null @@ -28,7 +29,7 @@ class EntityFormParseProcessor : BindAttributeProcessor, FormDefProcessor, Model override fun processModelAttribute(name: String, value: String) { version = value - if (BuildConfig.DEBUG && value.startsWith(V2025_1)) { + if (value.startsWith(V2025_1) && v2025enabled()) { return } diff --git a/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityXFormParserFactory.kt b/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityXFormParserFactory.kt index a24ad003a75..3e870edd0e6 100644 --- a/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityXFormParserFactory.kt +++ b/entities/src/main/java/org/odk/collect/entities/javarosa/parse/EntityXFormParserFactory.kt @@ -3,9 +3,12 @@ package org.odk.collect.entities.javarosa.parse import org.javarosa.xform.parse.IXFormParserFactory import org.javarosa.xform.parse.XFormParser -class EntityXFormParserFactory(base: IXFormParserFactory) : IXFormParserFactory.Wrapper(base) { +class EntityXFormParserFactory( + base: IXFormParserFactory, + private val v2025enabled: () -> Boolean +) : IXFormParserFactory.Wrapper(base) { override fun apply(parser: XFormParser): XFormParser { - val processor = EntityFormParseProcessor() + val processor = EntityFormParseProcessor(v2025enabled) parser.addProcessor(processor) return parser diff --git a/settings/src/main/java/org/odk/collect/settings/keys/ProjectKeys.kt b/settings/src/main/java/org/odk/collect/settings/keys/ProjectKeys.kt index efc04321a76..3755d0b3d67 100644 --- a/settings/src/main/java/org/odk/collect/settings/keys/ProjectKeys.kt +++ b/settings/src/main/java/org/odk/collect/settings/keys/ProjectKeys.kt @@ -54,6 +54,7 @@ object ProjectKeys { // experimental_preferences.xml const val KEY_DEBUG_FILTERS = "experimental_debug_filters" const val KEY_ZXING_SCANNING = "zxing_scanning" + const val KEY_ENTITIES_SPEC_V2025_1 = "entities_spec_v2025_1" // values const val PROTOCOL_SERVER = "odk_default" diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index a1a4ec7753b..9896cdf24ac 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -1229,7 +1229,8 @@ Entity lists - + + Enable entities spec v2025.1 View entity lists From a21def1f3f6778c6c810b1bb44149817c0f30246 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 14 Oct 2025 11:31:31 +0200 Subject: [PATCH 3/5] Fix existing tests --- .../odk/collect/entities/javarosa/EntitiesTest.java | 2 +- .../EntityFormFinalizationProcessorTest.java | 2 +- .../javarosa/EntityFormParseProcessorTest.java | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/entities/src/test/java/org/odk/collect/entities/javarosa/EntitiesTest.java b/entities/src/test/java/org/odk/collect/entities/javarosa/EntitiesTest.java index 8d85772d964..00b72181407 100644 --- a/entities/src/test/java/org/odk/collect/entities/javarosa/EntitiesTest.java +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/EntitiesTest.java @@ -39,7 +39,7 @@ public class EntitiesTest { - private final EntityXFormParserFactory entityXFormParserFactory = new EntityXFormParserFactory(new XFormParserFactory()); + private final EntityXFormParserFactory entityXFormParserFactory = new EntityXFormParserFactory(new XFormParserFactory(), () -> false); @Before public void setup() { 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 index 4dbebcf3cae..55a3a925f45 100644 --- a/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.java +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormFinalizationProcessorTest.java @@ -38,7 +38,7 @@ public class EntityFormFinalizationProcessorTest { - private final EntityXFormParserFactory entityXFormParserFactory = new EntityXFormParserFactory(new XFormParserFactory()); + private final EntityXFormParserFactory entityXFormParserFactory = new EntityXFormParserFactory(new XFormParserFactory(), () -> false); @Before public void setup() { 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 index 6f1f676be3b..eb11a1c2803 100644 --- a/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormParseProcessorTest.java +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormParseProcessorTest.java @@ -53,7 +53,7 @@ public void whenVersionIsMissing_parsesWithoutError() throws XFormParser.ParseEx ) ); - EntityFormParseProcessor processor = new EntityFormParseProcessor(); + EntityFormParseProcessor processor = new EntityFormParseProcessor(() -> false); XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); parser.addProcessor(processor); parser.parse(null); @@ -84,7 +84,7 @@ public void whenVersionIsMissing_andThereIsAnEntityElement_throwsException() { ) ); - EntityFormParseProcessor processor = new EntityFormParseProcessor(); + EntityFormParseProcessor processor = new EntityFormParseProcessor(() -> false); XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); parser.addProcessor(processor); @@ -125,7 +125,7 @@ public void whenVersionIsNotRecognized_throwsException() throws XFormParser.Pars ) ); - EntityFormParseProcessor processor = new EntityFormParseProcessor(); + EntityFormParseProcessor processor = new EntityFormParseProcessor(() -> false); XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); parser.addProcessor(processor); parser.parse(null); @@ -158,7 +158,7 @@ public void whenVersionIsNewPatch_parsesCorrectly() throws XFormParser.ParseExce ) ); - EntityFormParseProcessor processor = new EntityFormParseProcessor(); + EntityFormParseProcessor processor = new EntityFormParseProcessor(() -> false); XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); parser.addProcessor(processor); @@ -193,7 +193,7 @@ public void whenVersionIsNewVersionWithUpdates_parsesCorrectly() throws XFormPar ) ); - EntityFormParseProcessor processor = new EntityFormParseProcessor(); + EntityFormParseProcessor processor = new EntityFormParseProcessor(() -> false); XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); parser.addProcessor(processor); @@ -227,7 +227,7 @@ public void saveTosWithIncorrectNamespaceAreIgnored() throws XFormParser.ParseEx ) ); - EntityFormParseProcessor processor = new EntityFormParseProcessor(); + EntityFormParseProcessor processor = new EntityFormParseProcessor(() -> false); XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); parser.addProcessor(processor); From 3fd8aaed9dc27b87ce1a83ee4ccf607339c16491 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 14 Oct 2025 11:43:23 +0200 Subject: [PATCH 4/5] Add new tests --- .../EntityFormParseProcessorTest.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) 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 index eb11a1c2803..c687dda4195 100644 --- a/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormParseProcessorTest.java +++ b/entities/src/test/java/org/odk/collect/entities/javarosa/EntityFormParseProcessorTest.java @@ -234,4 +234,72 @@ public void saveTosWithIncorrectNamespaceAreIgnored() throws XFormParser.ParseEx FormDef formDef = parser.parse(null); assertThat(formDef.getExtras().get(EntityFormExtra.class).getSaveTos(), is(empty())); } + + @Test + public void whenVersionIs2025_1_andFeatureIsEnabled_parsesCorrectly() throws XFormParser.ParseException { + String updateVersion = "2025.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(() -> true); + XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); + parser.addProcessor(processor); + + FormDef formDef = parser.parse(null); + assertThat(formDef, notNullValue()); + } + + @Test(expected = UnrecognizedEntityVersionException.class) + public void whenVersionIs2025_1_andFeatureIsDisabled_throwsException() throws XFormParser.ParseException { + String updateVersion = "2025.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(() -> false); + XFormParser parser = new XFormParser(new InputStreamReader(new ByteArrayInputStream(form.asXml().getBytes()))); + parser.addProcessor(processor); + parser.parse(null); + } } From 01f5dc57a3221ffd69534b2da3fb63ab5fd8da72 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Thu, 16 Oct 2025 20:57:21 +0200 Subject: [PATCH 5/5] Use getExperimentalOptIn to determine if the spec is enabled --- .../android/application/initialization/JavaRosaInitializer.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 63143404a9d..cba2b3edccd 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 @@ -9,6 +9,7 @@ import org.javarosa.xform.parse.XFormParserFactory import org.javarosa.xform.util.XFormUtils import org.odk.collect.android.dynamicpreload.DynamicPreloadXFormParserFactory import org.odk.collect.android.logic.actions.setgeopoint.CollectSetGeopointActionHandler +import org.odk.collect.android.preferences.SettingsExt.getExperimentalOptIn import org.odk.collect.android.projects.ProjectsDataService import org.odk.collect.entities.javarosa.intance.LocalEntitiesExternalInstanceParserFactory import org.odk.collect.entities.javarosa.parse.EntityXFormParserFactory @@ -46,7 +47,7 @@ class JavaRosaInitializer( val entityXFormParserFactory = EntityXFormParserFactory( XFormParserFactory() - ) { settingsProvider.getUnprotectedSettings().getBoolean(ProjectKeys.KEY_ENTITIES_SPEC_V2025_1) } + ) { settingsProvider.getUnprotectedSettings().getExperimentalOptIn(ProjectKeys.KEY_ENTITIES_SPEC_V2025_1) } val dynamicPreloadXFormParserFactory = DynamicPreloadXFormParserFactory(entityXFormParserFactory)