From da7070b6fd881aad430c62023c8e45f45a39123e Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 21 Nov 2022 01:42:45 +0400 Subject: [PATCH 001/402] Create contributing.md --- .github/contributing.md | 53 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/contributing.md diff --git a/.github/contributing.md b/.github/contributing.md new file mode 100644 index 0000000000..bfd957eab0 --- /dev/null +++ b/.github/contributing.md @@ -0,0 +1,53 @@ +### How to Create PRs on CommCare Core + +[Formplayer](https://github.com/dimagi/formplayer) and [CommCare Android](https://github.com/dimagi/commcare-android) both utilizes commcare-core as the underlining XForm engine. To minimize the disruptions from changes on one platform to another, we maintain two different branches `formplayer` and `master` for Formplayer and CommCare Android respectively. +We try to keep both these branches in sync with each other the best we can. To do that it's cruicial that you PR your changes against both these branches. Please find more information below on how to create these PRs depending on what platform you work with - + + +##### Duplicating A Formplayer change to CommCare Android + +1. If you are working on a Formplayer change, you will want to start by checking out `your_feature_branch` from `formplayer` as the base branch. Make changes on `your_feature_branch` and create your original PR against `formplayer` branch. + +2. Now you will need to duplicate this PR by making another PR against `master`. To do this, firstly you would want to compare `your_feature_branch` to the `master` and see if the diff only shows the commit that belongs to your branch. If yes, you can directly create another PR from `your_feature_branch` to `master`. +Otherwise you will need to create another branch by checking out `your_feature_branch_dupe` from `master` as the base branch. You will then need to [cherry-pick](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/cherry-picking-a-commit#about-git-cherry-pick) each of the commits from `your_feature_branch` to `your_feature_branch_dupe`. +Subsequently you should create a PR from `your_feature_branch_dupe` against `master`. While creating this PR you can remove all the PR template field and simply mention your original `formplayer` PR as a reference. + +3. In order for us to test that your PR against `master` doesn't break anything on CommCare Android, we need to run android side tests with your PR. +To do this - + - Check out a new branch say `test_cc_1189` from `master` in [CommCare Android](https://github.com/dimagi/commcare-android) + - Do an empty commit on your new branch as `git commit --allow-empty -m "Empty-Commit"` and push your branch to github. + - Create a dummy test PR with a subject `[Test] CC 1189`. + - You can remove all the fields from the PR template here and add a line at the very bottom saying `cross-request: link_to_commcare-core_master_pr`. Without this line the tests won't factor in your CommCare Core `master` PR to run tests with. + - Label the PR with `cross requested tag` + - Label the PR with an appropriate `product/*` tag. + - You don't need to add any reviewers here as this PR will be closed without merging and is only used to run Android tests + + See https://github.com/dimagi/commcare-android/pull/2609 for an example of such a test PR. + +4. Go back to your CommCare Core `master` PR and add `cross-request: link_to_commcare-android_test_pr` as the last line in the PR description. This PR will now be owned by the Android devs to decide whether any additional changes are required on CommCare Android before merging this PR. + + +##### Duplicating A CommCare Android change to Formplayer + +1. If you are working on a CommCare Android change, you will want to start by checking out `your_feature_branch` from `master` as the base branch. Make changes on `your_feature_branch` and create your original PR against `master` branch. + +2. Now you will need to duplicate this PR by making another PR against `formplayer`. To do this, firstly you would want to compare `your_feature_branch` to the `formplayer` and see if the diff only shows the commit that belongs to your branch. If yes, you can directly create another PR from `your_feature_branch` to `formplayer`. +Otherwise you will need to create another branch by checking out `your_feature_branch_dupe` from `formplayer` as the base branch. You will then need to [cherry-pick](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/cherry-picking-a-commit#about-git-cherry-pick) each of the commits from `your_feature_branch` to `your_feature_branch_dupe`. +Subsequently you should create a PR from `your_feature_branch_dupe` against `formplayer`. While creating this PR you can remove all the PR template field and simply mention your original `master` PR as a reference. + +3. In order for us to test that your PR against `formplayer` doesn't break anything on Formplayer, we need to run formplayer side tests with your PR. +To do this - + - Check out a new branch say `test_cc_1189` from `master` in [Formplayer](https://github.com/dimagi/formplayer) + - Point the submodule in `libs/commcare` to your CommCare Core `your_feature_branch_dupe` branch and push your formplayer branch - +```` +cd libs/commcare +git fetch; git checkout your_feature_branch_dupe +cd ../.. +git add libs/commcare +git commit -m "update submodule to 1189 head" +git push origin test_cc_1189 +```` + - Create a dummy test PR in Formplayer with a subject `[Test] CC 1189`. + - You don't need to add any reviewers here as this PR will be closed without merging and is only used to run Formplayer tests + +4. Go back to your CommCare Core `formplayer` PR and add `cross-request: link_to_formplayer_test_pr` as the last line in the PR description. This PR will now be owned by the Formplayer devs to decide whether any additional changes are required on Formplayer before merging this PR. From 4e754658c5aeb1834a27bf5934d1b6b6f13f34fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Nov 2022 17:55:03 +0000 Subject: [PATCH 002/402] Bump junit from 4.12 to 4.13.2 Bumps [junit](https://github.com/junit-team/junit4) from 4.12 to 4.13.2. - [Release notes](https://github.com/junit-team/junit4/releases) - [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.12.md) - [Commits](https://github.com/junit-team/junit4/compare/r4.12...r4.13.2) --- updated-dependencies: - dependency-name: junit:junit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b8f4dd4095..75d6a36ee1 100644 --- a/build.gradle +++ b/build.gradle @@ -98,7 +98,7 @@ dependencies { testImplementation sourceSets.ccapi.output testImplementation sourceSets.cli.output testImplementation sourceSets.translate.output - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13.2' testImplementation 'org.json:json:20140107' testImplementation 'org.xerial:sqlite-jdbc:3.28.0' testImplementation 'joda-time:joda-time:2.9.4' From a7aaab87ea41aedee965e9cb9aaf7437d19cf718 Mon Sep 17 00:00:00 2001 From: Rynhard Date: Thu, 24 Nov 2022 15:57:58 +0200 Subject: [PATCH 003/402] Changed fromMillis to work with calls coming from commcare-android --- src/main/java/org/javarosa/xform/util/CalendarUtils.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/javarosa/xform/util/CalendarUtils.java b/src/main/java/org/javarosa/xform/util/CalendarUtils.java index 7582a133ab..e740afd75b 100644 --- a/src/main/java/org/javarosa/xform/util/CalendarUtils.java +++ b/src/main/java/org/javarosa/xform/util/CalendarUtils.java @@ -245,7 +245,7 @@ public static String convertToNepaliString(Date date, String format) { format = "%e %B %Y"; } - UniversalDate dateUniv = CalendarUtils.fromMillis(date); + UniversalDate dateUniv = CalendarUtils.fromMillis(date.getTime()); DateUtils.DateFields df = DateUtils.getFieldsForNonGregorianCalendar(dateUniv.year, dateUniv.month, dateUniv.day); @@ -358,8 +358,9 @@ public static UniversalDate fromMillis(Date date, String timezone) { return fromMillis(dateInMillis, timezoneObject); } - public static UniversalDate fromMillis(Date date) { - return fromMillis(date, null); + public static UniversalDate fromMillis(long millisFromJavaEpoch) { + Date date = new Date(millisFromJavaEpoch); + return fromMillis(date,null); } public static UniversalDate incrementMonth(UniversalDate date) { From 1eeb97d952ce14bdbf06b4630f4f89a6433c0450 Mon Sep 17 00:00:00 2001 From: Rynhard Date: Thu, 24 Nov 2022 16:05:09 +0200 Subject: [PATCH 004/402] stickler changes --- src/main/java/org/javarosa/xform/util/CalendarUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/javarosa/xform/util/CalendarUtils.java b/src/main/java/org/javarosa/xform/util/CalendarUtils.java index e740afd75b..8ea46de208 100644 --- a/src/main/java/org/javarosa/xform/util/CalendarUtils.java +++ b/src/main/java/org/javarosa/xform/util/CalendarUtils.java @@ -360,7 +360,7 @@ public static UniversalDate fromMillis(Date date, String timezone) { public static UniversalDate fromMillis(long millisFromJavaEpoch) { Date date = new Date(millisFromJavaEpoch); - return fromMillis(date,null); + return fromMillis(date, null); } public static UniversalDate incrementMonth(UniversalDate date) { From 61184517c74cad3170d294e4c73fc6a8eaa8accc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Nov 2022 11:31:51 +0000 Subject: [PATCH 005/402] Bump joda-time from 2.9.4 to 2.12.1 Bumps [joda-time](https://github.com/JodaOrg/joda-time) from 2.9.4 to 2.12.1. - [Release notes](https://github.com/JodaOrg/joda-time/releases) - [Changelog](https://github.com/JodaOrg/joda-time/blob/main/RELEASE-NOTES.txt) - [Commits](https://github.com/JodaOrg/joda-time/compare/v2.9.4...v2.12.1) --- updated-dependencies: - dependency-name: joda-time:joda-time dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 75d6a36ee1..707842cb09 100644 --- a/build.gradle +++ b/build.gradle @@ -73,7 +73,7 @@ dependencies { // Added so Android Studio recognizes libs in util jar projects implementation 'org.json:json:20140107' implementation 'commons-cli:commons-cli:1.3.1' - implementation 'joda-time:joda-time:2.9.4' + implementation 'joda-time:joda-time:2.12.1' implementation 'com.carrotsearch:hppc:0.7.2' api 'com.squareup.retrofit2:retrofit:2.3.0' api 'com.squareup.okhttp3:okhttp:3.12.12' From 1de6308c2067ebc2a841f02ff562d7372bf503a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Nov 2022 11:40:24 +0000 Subject: [PATCH 006/402] Bump rxjava from 2.1.1 to 2.2.21 Bumps [rxjava](https://github.com/ReactiveX/RxJava) from 2.1.1 to 2.2.21. - [Release notes](https://github.com/ReactiveX/RxJava/releases) - [Changelog](https://github.com/ReactiveX/RxJava/blob/v2.2.21/CHANGES.md) - [Commits](https://github.com/ReactiveX/RxJava/compare/v2.1.1...v2.2.21) --- updated-dependencies: - dependency-name: io.reactivex.rxjava2:rxjava dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 707842cb09..5fda31fc1f 100644 --- a/build.gradle +++ b/build.gradle @@ -78,7 +78,7 @@ dependencies { api 'com.squareup.retrofit2:retrofit:2.3.0' api 'com.squareup.okhttp3:okhttp:3.12.12' implementation 'com.google.code.findbugs:jsr305:3.0.2' - implementation 'io.reactivex.rxjava2:rxjava:2.1.1' + implementation 'io.reactivex.rxjava2:rxjava:2.2.21' implementation 'com.google.guava:guava:30.1.1-jre' implementation 'io.opentracing:opentracing-api:0.33.0' implementation 'io.opentracing:opentracing-util:0.33.0' From 1c636ee952d13287f9e7348940167c7de42e4fbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Nov 2022 12:31:24 +0000 Subject: [PATCH 007/402] Bump sqlite-jdbc from 3.28.0 to 3.40.0.0 Bumps [sqlite-jdbc](https://github.com/xerial/sqlite-jdbc) from 3.28.0 to 3.40.0.0. - [Release notes](https://github.com/xerial/sqlite-jdbc/releases) - [Changelog](https://github.com/xerial/sqlite-jdbc/blob/master/CHANGELOG) - [Commits](https://github.com/xerial/sqlite-jdbc/compare/3.28.0...3.40.0.0) --- updated-dependencies: - dependency-name: org.xerial:sqlite-jdbc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5fda31fc1f..cf29da7a23 100644 --- a/build.gradle +++ b/build.gradle @@ -100,7 +100,7 @@ dependencies { testImplementation sourceSets.translate.output testImplementation 'junit:junit:4.13.2' testImplementation 'org.json:json:20140107' - testImplementation 'org.xerial:sqlite-jdbc:3.28.0' + testImplementation 'org.xerial:sqlite-jdbc:3.40.0.0' testImplementation 'joda-time:joda-time:2.9.4' testImplementation 'com.squareup.retrofit2:retrofit:2.3.0' testImplementation 'com.squareup.okhttp3:okhttp:4.4.1' From db47983cc186dd6f0844f453826aef0c6bb7957a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Nov 2022 00:43:56 +0000 Subject: [PATCH 008/402] Bump json from 20140107 to 20220924 Bumps [json](https://github.com/douglascrockford/JSON-java) from 20140107 to 20220924. - [Release notes](https://github.com/douglascrockford/JSON-java/releases) - [Changelog](https://github.com/stleary/JSON-java/blob/master/docs/RELEASES.md) - [Commits](https://github.com/douglascrockford/JSON-java/commits) --- updated-dependencies: - dependency-name: org.json:json dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cf29da7a23..f04eaec1bb 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ dependencies { api 'com.github.stefanhaustein:kxml2:2.4.1' implementation 'javax.ws.rs:javax.ws.rs-api:2.0.1' // Added so Android Studio recognizes libs in util jar projects - implementation 'org.json:json:20140107' + implementation 'org.json:json:20220924' implementation 'commons-cli:commons-cli:1.3.1' implementation 'joda-time:joda-time:2.12.1' implementation 'com.carrotsearch:hppc:0.7.2' From 6d0cef41b6b2446674b8b19dbde55af6d29d54db Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 30 Nov 2022 19:22:22 +0400 Subject: [PATCH 009/402] trigger dependabot version updates for formplayer --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 47c3fccf29..8de29895ba 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,3 +6,12 @@ updates: interval: daily labels: - dependencies + + # Copy of above config for formplayer + - package-ecosystem: gradle + directory: "/" + target-branch: "formplayer" + schedule: + interval: daily + labels: + - dependencies From 3019988f4f651f07bc234b81174e3502b853505e Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Wed, 7 Dec 2022 14:37:30 +0200 Subject: [PATCH 010/402] update data instance toString methods to provide more info --- .../org/javarosa/core/model/instance/DataInstance.java | 6 +++++- .../core/model/instance/ExternalDataInstance.java | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/javarosa/core/model/instance/DataInstance.java b/src/main/java/org/javarosa/core/model/instance/DataInstance.java index ae6f4659c0..4587efa336 100644 --- a/src/main/java/org/javarosa/core/model/instance/DataInstance.java +++ b/src/main/java/org/javarosa/core/model/instance/DataInstance.java @@ -245,12 +245,16 @@ public void setName(String name) { this.name = name; } + @Override public String toString() { String name = "NULL"; if (this.name != null) { name = this.name; } - return name; + return "DataInstance{" + + "name='" + name + '\'' + + ", instanceid='" + instanceid + '\'' + + '}'; } @Override diff --git a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java index a2bbd77dc4..8ae65e13ce 100644 --- a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java +++ b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java @@ -171,4 +171,13 @@ public void copyFromSource(ExternalDataInstanceSource source) { public ExternalDataInstance copy() { return new ExternalDataInstance(this); } + + @Override + public String toString() { + return "ExternalDataInstance{" + + "reference='" + reference + '\'' + + ", name='" + name + '\'' + + ", instanceid='" + instanceid + '\'' + + '}'; + } } From 9097c8859bbf157f16126d4a28b6d98847ab6312 Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Wed, 7 Dec 2022 14:38:55 +0200 Subject: [PATCH 011/402] user 'serializeNode' to include node text The 'serialize' method assumes that the node is a 'root' or 'parent' node and won't serialize the node text if it has any. This means it doesn't work well for serializing a nodeset which has no 'root' element. --- .../java/org/javarosa/model/xform/DataModelSerializer.java | 4 ++-- src/main/java/org/javarosa/xpath/expr/XPathExpression.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/javarosa/model/xform/DataModelSerializer.java b/src/main/java/org/javarosa/model/xform/DataModelSerializer.java index 3efc6f59ec..387640ac5f 100644 --- a/src/main/java/org/javarosa/model/xform/DataModelSerializer.java +++ b/src/main/java/org/javarosa/model/xform/DataModelSerializer.java @@ -61,7 +61,7 @@ public void serialize(AbstractTreeElement root) throws IOException { serializer.flush(); } - private void serializeNode(AbstractTreeElement instanceNode) throws IOException { + public void serializeNode(AbstractTreeElement instanceNode) throws IOException { //don't serialize template nodes or non-relevant nodes if (!instanceNode.isRelevant() || instanceNode.getMult() == TreeReference.INDEX_TEMPLATE) { return; @@ -88,4 +88,4 @@ private void serializeAttributes(AbstractTreeElement instanceNode) throws IOExce serializer.attribute(instanceNode.getAttributeNamespace(i), instanceNode.getAttributeName(i), val); } } -} \ No newline at end of file +} diff --git a/src/main/java/org/javarosa/xpath/expr/XPathExpression.java b/src/main/java/org/javarosa/xpath/expr/XPathExpression.java index 86e2b27ffe..c395360e19 100755 --- a/src/main/java/org/javarosa/xpath/expr/XPathExpression.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathExpression.java @@ -91,8 +91,9 @@ private static void serializeElements(XPathNodeset nodeset, OutputStream output) for (TreeReference ref : refs) { AbstractTreeElement treeElement = instance.resolveReference(ref); - s.serialize(treeElement); + s.serializeNode(treeElement); } + serializer.flush(); } /** From 4fdb080124ab62afe5a9f7978d5f39472880502a Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Wed, 7 Dec 2022 14:37:30 +0200 Subject: [PATCH 012/402] update data instance toString methods to provide more info --- .../org/javarosa/core/model/instance/DataInstance.java | 6 +++++- .../core/model/instance/ExternalDataInstance.java | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/javarosa/core/model/instance/DataInstance.java b/src/main/java/org/javarosa/core/model/instance/DataInstance.java index ae6f4659c0..4587efa336 100644 --- a/src/main/java/org/javarosa/core/model/instance/DataInstance.java +++ b/src/main/java/org/javarosa/core/model/instance/DataInstance.java @@ -245,12 +245,16 @@ public void setName(String name) { this.name = name; } + @Override public String toString() { String name = "NULL"; if (this.name != null) { name = this.name; } - return name; + return "DataInstance{" + + "name='" + name + '\'' + + ", instanceid='" + instanceid + '\'' + + '}'; } @Override diff --git a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java index a2bbd77dc4..8ae65e13ce 100644 --- a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java +++ b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java @@ -171,4 +171,13 @@ public void copyFromSource(ExternalDataInstanceSource source) { public ExternalDataInstance copy() { return new ExternalDataInstance(this); } + + @Override + public String toString() { + return "ExternalDataInstance{" + + "reference='" + reference + '\'' + + ", name='" + name + '\'' + + ", instanceid='" + instanceid + '\'' + + '}'; + } } From 003c3c1c339e7e0d386bfa94fdda6d6c4f18ccb1 Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Wed, 7 Dec 2022 14:38:55 +0200 Subject: [PATCH 013/402] user 'serializeNode' to include node text The 'serialize' method assumes that the node is a 'root' or 'parent' node and won't serialize the node text if it has any. This means it doesn't work well for serializing a nodeset which has no 'root' element. --- .../java/org/javarosa/model/xform/DataModelSerializer.java | 4 ++-- src/main/java/org/javarosa/xpath/expr/XPathExpression.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/javarosa/model/xform/DataModelSerializer.java b/src/main/java/org/javarosa/model/xform/DataModelSerializer.java index 3efc6f59ec..387640ac5f 100644 --- a/src/main/java/org/javarosa/model/xform/DataModelSerializer.java +++ b/src/main/java/org/javarosa/model/xform/DataModelSerializer.java @@ -61,7 +61,7 @@ public void serialize(AbstractTreeElement root) throws IOException { serializer.flush(); } - private void serializeNode(AbstractTreeElement instanceNode) throws IOException { + public void serializeNode(AbstractTreeElement instanceNode) throws IOException { //don't serialize template nodes or non-relevant nodes if (!instanceNode.isRelevant() || instanceNode.getMult() == TreeReference.INDEX_TEMPLATE) { return; @@ -88,4 +88,4 @@ private void serializeAttributes(AbstractTreeElement instanceNode) throws IOExce serializer.attribute(instanceNode.getAttributeNamespace(i), instanceNode.getAttributeName(i), val); } } -} \ No newline at end of file +} diff --git a/src/main/java/org/javarosa/xpath/expr/XPathExpression.java b/src/main/java/org/javarosa/xpath/expr/XPathExpression.java index 86e2b27ffe..c395360e19 100755 --- a/src/main/java/org/javarosa/xpath/expr/XPathExpression.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathExpression.java @@ -91,8 +91,9 @@ private static void serializeElements(XPathNodeset nodeset, OutputStream output) for (TreeReference ref : refs) { AbstractTreeElement treeElement = instance.resolveReference(ref); - s.serialize(treeElement); + s.serializeNode(treeElement); } + serializer.flush(); } /** From 33dadf2a6bde6079ef98528a43664543c945ba0e Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 13 Dec 2022 11:12:11 +0200 Subject: [PATCH 014/402] Bump JDK source and target to version 17 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f04eaec1bb..a41d63aca4 100644 --- a/build.gradle +++ b/build.gradle @@ -13,8 +13,8 @@ plugins { } // Needed even though it looks unused -sourceCompatibility = 11 -targetCompatibility = JavaVersion.VERSION_11 +sourceCompatibility = JavaVersion.VERSION_17 +targetCompatibility = JavaVersion.VERSION_17 configurations { ccapi.extendsFrom(ccapiImplementation) From eccfaa3270a90490c0daea16e11cfebeb57d9a5b Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 13 Dec 2022 11:46:22 +0200 Subject: [PATCH 015/402] Set Java version of Github build to 17 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2633441e38..4769769fb2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: build: runs-on: ubuntu-latest env: - JAVA_VERSION: '11' + JAVA_VERSION: '17' JOB_GRADLE_VERSION: 7.3 FORMPLAYER_GRADLE_VERSION: 7.3 FORMPLAYER_JAVA_VERSION: '8' From 8ecaac187f01e14b966943fa8e237df677d265b4 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 13 Dec 2022 21:14:04 +0200 Subject: [PATCH 016/402] Bump sqlite-jdbc to version 3.40.0.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a41d63aca4..d72b460716 100644 --- a/build.gradle +++ b/build.gradle @@ -108,7 +108,7 @@ dependencies { ccapiImplementation sourceSets.main.output ccapiImplementation 'org.json:json:20140107' - ccapiImplementation 'org.xerial:sqlite-jdbc:3.28.0' + ccapiImplementation 'org.xerial:sqlite-jdbc:3.40.0.0' ccapiImplementation 'com.github.stefanhaustein:kxml2:2.4.1' ccapiImplementation 'joda-time:joda-time:2.9.4' ccapiImplementation 'com.carrotsearch:hppc:0.7.2' From 4b256855deb4327e588bda589a14e16c5cdbe612 Mon Sep 17 00:00:00 2001 From: Steph Date: Wed, 7 Dec 2022 14:21:52 -0500 Subject: [PATCH 017/402] return flase if selectionSize is 0 rather than throwing an error --- .../commcare/util/screen/MultiSelectEntityScreen.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java index 15872918cf..ced5fb75ad 100644 --- a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java @@ -89,7 +89,7 @@ public boolean autoSelectEntities(SessionWrapper session) { private boolean validateSelectionSize(int selectionSize) { if (selectionSize == 0) { - throw new InvalidEntitiesSelectionException(getNoEntitiesError()); + return false; } else if (selectionSize > maxSelectValue) { throw new InvalidEntitiesSelectionException(getMaxSelectError(selectionSize)); } @@ -117,14 +117,6 @@ private String getMaxSelectError(int selectionSize) { return error; } - private String getNoEntitiesError() { - try { - return Localization.get("case.list.no.selection.error"); - } catch (NoLocalizedTextException | NullPointerException e) { - return String.format("No cases found"); - } - } - private void setSelectedEntities(String input, @Nullable String[] selectedValues) throws CommCareSessionException { if (input.contentEquals(USE_SELECTED_VALUES)) { From fe874943633d6ddc96cb3a07ccb3419b2b532a31 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 14 Dec 2022 20:45:00 +0400 Subject: [PATCH 018/402] disable auto select if there are no cases --- .../java/org/commcare/util/screen/MultiSelectEntityScreen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java index ced5fb75ad..1a6804837e 100644 --- a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java @@ -69,7 +69,7 @@ public void updateSelection(String input, TreeReference[] selectedRefs) @Override protected boolean shouldAutoSelect() { - return mNeededDatum.isAutoSelectEnabled(); + return mNeededDatum.isAutoSelectEnabled() && references.size() != 0; } @Override From 99f5cee96db100b37fd12af1023480d22260e7ab Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 14 Dec 2022 17:28:25 +0200 Subject: [PATCH 019/402] Fix deprecation warnings --- .../cases/ledger/LedgerPurgeFilter.java | 2 +- .../commcare/cases/util/CasePurgeFilter.java | 4 +- .../java/org/commcare/xml/GridParser.java | 2 +- .../java/org/commcare/xml/MarkupParser.java | 2 +- .../java/org/commcare/xml/StyleParser.java | 2 +- .../model/condition/EvaluationContext.java | 4 +- .../javarosa/core/model/data/DecimalData.java | 2 +- .../javarosa/core/model/data/LongData.java | 2 +- .../javarosa/core/model/utils/DateUtils.java | 4 +- .../javarosa/form/api/FormEntryCaption.java | 2 +- .../org/javarosa/form/api/FormEntryModel.java | 3 +- .../org/javarosa/xml/TreeElementParser.java | 2 +- .../org/javarosa/xpath/XPathLazyNodeset.java | 4 +- .../javarosa/xpath/expr/FunctionUtils.java | 20 +- .../javarosa/xpath/expr/XPathArithExpr.java | 2 +- .../javarosa/xpath/expr/XPathCeilingFunc.java | 2 +- .../org/javarosa/xpath/expr/XPathCmpExpr.java | 4 +- .../javarosa/xpath/expr/XPathCountFunc.java | 2 +- .../xpath/expr/XPathCountSelectedFunc.java | 2 +- .../xpath/expr/XPathDistanceFunc.java | 4 +- .../javarosa/xpath/expr/XPathFloorFunc.java | 2 +- .../javarosa/xpath/expr/XPathIndexOfFunc.java | 2 +- .../xpath/expr/XPathNumericLiteral.java | 4 +- .../xpath/expr/XPathPositionFunc.java | 4 +- .../javarosa/xpath/expr/XPathRandomFunc.java | 2 +- .../javarosa/xpath/expr/XPathRoundFunc.java | 2 +- .../xpath/expr/XPathStringLengthFunc.java | 4 +- .../xpath/expr/XPathTranslateFunc.java | 6 +- .../model/data/test/IntegerDataTests.java | 4 +- .../core/model/test/ConstraintTest.java | 4 +- .../core/util/test/ExternalizableTest.java | 56 ++-- .../core/util/test/NumericEncodingTest.java | 2 +- .../test/XFormAnswerDataSerializerTest.java | 2 +- .../javarosa/xpath/test/XPathEvalTest.java | 284 +++++++++--------- .../java/com/csvreader/CsvReader.java | 4 +- 35 files changed, 227 insertions(+), 226 deletions(-) diff --git a/src/main/java/org/commcare/cases/ledger/LedgerPurgeFilter.java b/src/main/java/org/commcare/cases/ledger/LedgerPurgeFilter.java index caf8c62f33..35581b05b9 100644 --- a/src/main/java/org/commcare/cases/ledger/LedgerPurgeFilter.java +++ b/src/main/java/org/commcare/cases/ledger/LedgerPurgeFilter.java @@ -30,7 +30,7 @@ public LedgerPurgeFilter(IStorageUtilityIndexed ledgerStorage, IStorageU try { caseStorage.getRecordForValue(Case.INDEX_CASE_ID, s.getEntiyId()); } catch (NoSuchElementException nsee) { - idsToRemove.addElement(new Integer(s.getID())); + idsToRemove.addElement(Integer.valueOf(s.getID())); } } } diff --git a/src/main/java/org/commcare/cases/util/CasePurgeFilter.java b/src/main/java/org/commcare/cases/util/CasePurgeFilter.java index b03afa9057..b4b0df946f 100644 --- a/src/main/java/org/commcare/cases/util/CasePurgeFilter.java +++ b/src/main/java/org/commcare/cases/util/CasePurgeFilter.java @@ -104,7 +104,7 @@ private void setIdsToRemoveWithNewExtensions(DAG graph) t for (Enumeration iterator = internalCaseDAG.getNodes(); iterator.hasMoreElements(); ) { int[] node = (int[])iterator.nextElement(); if (!caseStatusIs(node[0], STATUS_ALIVE)) { - idsToRemove.addElement(new Integer(node[1])); + idsToRemove.addElement(Integer.valueOf(node[1])); } } } @@ -337,7 +337,7 @@ private void removeNodeAndPropagate(String indexOfRemovedNode, // DAG, and add it to the list of cases to be purged if (casesRemovedDueToMissingCases.indexOf(indexOfRemovedNode) == -1) { int storageIdOfRemovedNode = internalCaseDAG.removeNode(indexOfRemovedNode)[1]; - idsToRemove.addElement(new Integer(storageIdOfRemovedNode)); + idsToRemove.addElement(Integer.valueOf(storageIdOfRemovedNode)); casesRemovedDueToMissingCases.addElement(indexOfRemovedNode); } } diff --git a/src/main/java/org/commcare/xml/GridParser.java b/src/main/java/org/commcare/xml/GridParser.java index 36ac7295e4..9bc25e9810 100644 --- a/src/main/java/org/commcare/xml/GridParser.java +++ b/src/main/java/org/commcare/xml/GridParser.java @@ -42,6 +42,6 @@ public Integer parse() throws InvalidStructureException, IOException, XmlPullPar //exit grid block parser.nextTag(); - return new Integer(1); + return Integer.valueOf(1); } } diff --git a/src/main/java/org/commcare/xml/MarkupParser.java b/src/main/java/org/commcare/xml/MarkupParser.java index f09135380c..aa0f1d2d05 100644 --- a/src/main/java/org/commcare/xml/MarkupParser.java +++ b/src/main/java/org/commcare/xml/MarkupParser.java @@ -29,6 +29,6 @@ public Integer parse() throws InvalidStructureException, IOException, XmlPullPar //exit grid block parser.nextTag(); - return new Integer(1); + return Integer.valueOf(1); } } diff --git a/src/main/java/org/commcare/xml/StyleParser.java b/src/main/java/org/commcare/xml/StyleParser.java index e8b1654e4b..b04c4fbccf 100644 --- a/src/main/java/org/commcare/xml/StyleParser.java +++ b/src/main/java/org/commcare/xml/StyleParser.java @@ -42,6 +42,6 @@ public Integer parse() throws InvalidStructureException, IOException, XmlPullPar parser.nextTag(); - return new Integer(1); + return Integer.valueOf(1); } } diff --git a/src/main/java/org/javarosa/core/model/condition/EvaluationContext.java b/src/main/java/org/javarosa/core/model/condition/EvaluationContext.java index a1ef0737ff..c6b4b6d55f 100755 --- a/src/main/java/org/javarosa/core/model/condition/EvaluationContext.java +++ b/src/main/java/org/javarosa/core/model/condition/EvaluationContext.java @@ -274,11 +274,11 @@ public void setVariable(String name, Object value) { //Some datatypes can be trivially converted to a first order //xpath datatype if (value instanceof Integer) { - variables.put(name, new Double(((Integer)value).doubleValue())); + variables.put(name, Double.valueOf(((Integer)value).doubleValue())); return; } if (value instanceof Float) { - variables.put(name, new Double(((Float)value).doubleValue())); + variables.put(name, Double.valueOf(((Float)value).doubleValue())); } else { //Otherwise we just hope for the best, I suppose? Should we log this? variables.put(name, value); diff --git a/src/main/java/org/javarosa/core/model/data/DecimalData.java b/src/main/java/org/javarosa/core/model/data/DecimalData.java index 77d35d491c..7161efcbec 100755 --- a/src/main/java/org/javarosa/core/model/data/DecimalData.java +++ b/src/main/java/org/javarosa/core/model/data/DecimalData.java @@ -45,7 +45,7 @@ public String getDisplayText() { @Override public Object getValue() { - return new Double(d); + return Double.valueOf(d); } @Override diff --git a/src/main/java/org/javarosa/core/model/data/LongData.java b/src/main/java/org/javarosa/core/model/data/LongData.java index 7e252efef0..ea839dff0e 100644 --- a/src/main/java/org/javarosa/core/model/data/LongData.java +++ b/src/main/java/org/javarosa/core/model/data/LongData.java @@ -67,7 +67,7 @@ public void writeExternal(DataOutputStream out) throws IOException { @Override public UncastData uncast() { - return new UncastData(new Long(n).toString()); + return new UncastData(Long.valueOf(n).toString()); } @Override diff --git a/src/main/java/org/javarosa/core/model/utils/DateUtils.java b/src/main/java/org/javarosa/core/model/utils/DateUtils.java index 08496c2fa7..30b13b78fc 100755 --- a/src/main/java/org/javarosa/core/model/utils/DateUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/DateUtils.java @@ -331,7 +331,7 @@ private static String formatDateISO8601(DateFields f) { } private static String formatDateColloquial(DateFields f) { - String year = new Integer(f.year).toString(); + String year = Integer.valueOf(f.year).toString(); //Normal Date if (year.length() == 4) { @@ -869,7 +869,7 @@ public static Double fractionalDaysSinceEpoch(Date a) { //correct for any drift in the offsets. This can also present if timezone definitions //have drifted over time long timeZoneAdjust = (a.getTimezoneOffset() - EPOCH_DATE.getTimezoneOffset()) * 60* 1000; - return new Double(((a.getTime() - EPOCH_DATE.getTime()) - timeZoneAdjust) / (double)DAY_IN_MS); + return Double.valueOf(((a.getTime() - EPOCH_DATE.getTime()) - timeZoneAdjust) / (double)DAY_IN_MS); } /** diff --git a/src/main/java/org/javarosa/form/api/FormEntryCaption.java b/src/main/java/org/javarosa/form/api/FormEntryCaption.java index a13b7364f0..93a4267fbf 100644 --- a/src/main/java/org/javarosa/form/api/FormEntryCaption.java +++ b/src/main/java/org/javarosa/form/api/FormEntryCaption.java @@ -261,7 +261,7 @@ public String getRepeatText(String typeKey) { Hashtable vars = new Hashtable<>(); vars.put("name", title); - vars.put("n", new Integer(count)); + vars.put("n", Integer.valueOf(count)); return form.fillTemplateString(caption, index.getReference(), vars); } diff --git a/src/main/java/org/javarosa/form/api/FormEntryModel.java b/src/main/java/org/javarosa/form/api/FormEntryModel.java index 72b335edf5..757d4b7c36 100644 --- a/src/main/java/org/javarosa/form/api/FormEntryModel.java +++ b/src/main/java/org/javarosa/form/api/FormEntryModel.java @@ -565,7 +565,8 @@ private boolean setRepeatNextMultiplicity(Vector elements, Vector< TreeElement parentNode = form.getMainInstance().resolveReference(nodeRef.getParentRef()); mult = parentNode.getChildMultiplicity(name); } - multiplicities.setElementAt(new Integer(repeatStructure == REPEAT_STRUCTURE_NON_LINEAR ? TreeReference.INDEX_REPEAT_JUNCTURE : mult), multiplicities.size() - 1); + multiplicities.setElementAt(Integer.valueOf(repeatStructure == REPEAT_STRUCTURE_NON_LINEAR ? + TreeReference.INDEX_REPEAT_JUNCTURE : mult), multiplicities.size() - 1); return true; } else { return false; diff --git a/src/main/java/org/javarosa/xml/TreeElementParser.java b/src/main/java/org/javarosa/xml/TreeElementParser.java index c6225f97cc..87e0505389 100644 --- a/src/main/java/org/javarosa/xml/TreeElementParser.java +++ b/src/main/java/org/javarosa/xml/TreeElementParser.java @@ -47,7 +47,7 @@ public TreeElement parse() throws InvalidStructureException, IOException, } else { val = 0; } - multiplicities.put(name, new Integer(val)); + multiplicities.put(name, Integer.valueOf(val)); TreeElement kid = new TreeElementParser(parser, val, instanceId).parse(); element.addChild(kid); diff --git a/src/main/java/org/javarosa/xpath/XPathLazyNodeset.java b/src/main/java/org/javarosa/xpath/XPathLazyNodeset.java index e8d39b5341..d3faaaaeef 100644 --- a/src/main/java/org/javarosa/xpath/XPathLazyNodeset.java +++ b/src/main/java/org/javarosa/xpath/XPathLazyNodeset.java @@ -30,7 +30,7 @@ public class XPathLazyNodeset extends XPathNodeset { //Since we're using this as a lock, we need to be very careful to ensure that each //nodeset gets its own new object. - private Boolean evaluated = new Boolean(false); + private Boolean evaluated = Boolean.valueOf(false); private final TreeReference unExpandedRef; /** @@ -57,7 +57,7 @@ private void performEvaluation() { } } this.setReferences(nodes); - evaluated = new Boolean(true); + evaluated = Boolean.valueOf(true); } } diff --git a/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java b/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java index 3799f03f44..be30e0c724 100644 --- a/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java +++ b/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java @@ -141,7 +141,7 @@ public static Object InferType(String attrValue) { try { // Don't process strings with scientific notation or +/- Infinity as doubles if (checkForInvalidNumericOrDatestringCharacters(attrValue)) { - mDoubleParseCache.register(attrValue, new Double(Double.NaN)); + mDoubleParseCache.register(attrValue, Double.valueOf(Double.NaN)); return attrValue; } Double ret = Double.parseDouble(attrValue); @@ -149,7 +149,7 @@ public static Object InferType(String attrValue) { return ret; } catch (NumberFormatException ife) { //Not a double - mDoubleParseCache.register(attrValue, new Double(Double.NaN)); + mDoubleParseCache.register(attrValue, Double.valueOf(Double.NaN)); } //TODO: What about dates? That is a _super_ expensive //operation to be testing, though... @@ -223,25 +223,25 @@ public static Double toNumeric(Object o) { o = unpack(o); if (o instanceof Boolean) { - val = new Double(((Boolean)o).booleanValue() ? 1 : 0); + val = Double.valueOf(((Boolean)o).booleanValue() ? 1 : 0); } else if (o instanceof Double) { val = (Double)o; } else if (o instanceof String) { String s = ((String)o).trim(); if (checkForInvalidNumericOrDatestringCharacters(s)) { - return new Double(Double.NaN); + return Double.valueOf(Double.NaN); } try { - val = new Double(Double.parseDouble(s)); + val = Double.valueOf(Double.parseDouble(s)); } catch (NumberFormatException nfe) { try { val = attemptDateConversion(s); } catch (XPathTypeMismatchException e) { - val = new Double(Double.NaN); + val = Double.valueOf(Double.NaN); } } } else if (o instanceof Date) { - val = new Double(DateUtils.daysSinceEpoch((Date)o)); + val = Double.valueOf(DateUtils.daysSinceEpoch((Date)o)); } else if (o instanceof IExprDataType) { val = ((IExprDataType)o).toNumeric(); } @@ -290,9 +290,9 @@ public static Double toInt(Object o) { return val; } else { long l = val.longValue(); - Double dbl = new Double(l); - if (l == 0 && (val < 0. || val.equals(new Double(-0.)))) { - dbl = new Double(-0.); + Double dbl = Double.valueOf(l); + if (l == 0 && (val < 0. || val.equals(Double.valueOf(-0.)))) { + dbl = Double.valueOf(-0.); } return dbl; } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathArithExpr.java b/src/main/java/org/javarosa/xpath/expr/XPathArithExpr.java index e806e8f931..e4b1dc1aa9 100755 --- a/src/main/java/org/javarosa/xpath/expr/XPathArithExpr.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathArithExpr.java @@ -41,7 +41,7 @@ protected Object evalRaw(DataInstance model, EvaluationContext evalContext) { result = aval % bval; break; } - return new Double(result); + return Double.valueOf(result); } @Override diff --git a/src/main/java/org/javarosa/xpath/expr/XPathCeilingFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathCeilingFunc.java index e4c5c66496..817aba947f 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathCeilingFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathCeilingFunc.java @@ -19,7 +19,7 @@ public XPathCeilingFunc(XPathExpression[] args) throws XPathSyntaxException { @Override public Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) { - return new Double(Math.ceil(FunctionUtils.toDouble(evaluatedArgs[0]))); + return Double.valueOf(Math.ceil(FunctionUtils.toDouble(evaluatedArgs[0]))); } } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathCmpExpr.java b/src/main/java/org/javarosa/xpath/expr/XPathCmpExpr.java index 70255e2761..524faf76b6 100755 --- a/src/main/java/org/javarosa/xpath/expr/XPathCmpExpr.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathCmpExpr.java @@ -114,9 +114,9 @@ private boolean handled(Object a, Object b, Object sentinal, Vector pivo } else if (b instanceof Float) { val = ((Float)b).doubleValue(); } else if (b instanceof Short) { - val = new Double((Short)b); + val = Double.valueOf((Short)b); } else if (b instanceof Byte) { - val = new Double((Byte)b); + val = Double.valueOf((Byte)b); } else { if (b instanceof String) { try { diff --git a/src/main/java/org/javarosa/xpath/expr/XPathCountFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathCountFunc.java index d61e75277b..22b0c2fe83 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathCountFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathCountFunc.java @@ -22,7 +22,7 @@ public XPathCountFunc(XPathExpression[] args) throws XPathSyntaxException { @Override public Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) { if (evaluatedArgs[0] instanceof XPathNodeset) { - return new Double(((XPathNodeset)evaluatedArgs[0]).size()); + return Double.valueOf(((XPathNodeset)evaluatedArgs[0]).size()); } else { throw new XPathTypeMismatchException("uses an invalid reference inside a count function"); } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathCountSelectedFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathCountSelectedFunc.java index b29310d934..b9b0dca42a 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathCountSelectedFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathCountSelectedFunc.java @@ -33,7 +33,7 @@ public Object evalBody(DataInstance model, EvaluationContext evalContext, Object } String s = (String)evalResult; - return new Double(DataUtil.splitOnSpaces(s).length); + return Double.valueOf(DataUtil.splitOnSpaces(s).length); } } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathDistanceFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathDistanceFunc.java index 11b19ae478..3ed8c808ec 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathDistanceFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathDistanceFunc.java @@ -37,7 +37,7 @@ public static Double distance(Object from, Object to) { String unpackedTo = (String)FunctionUtils.unpack(to); if (unpackedFrom == null || "".equals(unpackedFrom) || unpackedTo == null || "".equals(unpackedTo)) { - return new Double(-1.0); + return Double.valueOf(-1.0); } try { @@ -45,7 +45,7 @@ public static Double distance(Object from, Object to) { GeoPointData castedFrom = new GeoPointData().cast(new UncastData(unpackedFrom)); GeoPointData castedTo = new GeoPointData().cast(new UncastData(unpackedTo)); - return new Double(GeoPointUtils.computeDistanceBetween(castedFrom, castedTo)); + return Double.valueOf(GeoPointUtils.computeDistanceBetween(castedFrom, castedTo)); } catch (NumberFormatException e) { throw new XPathTypeMismatchException("distance() function requires arguments containing " + "numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo); diff --git a/src/main/java/org/javarosa/xpath/expr/XPathFloorFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathFloorFunc.java index 7ab9a60740..334015b6da 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathFloorFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathFloorFunc.java @@ -19,7 +19,7 @@ public XPathFloorFunc(XPathExpression[] args) throws XPathSyntaxException { @Override public Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) { - return new Double(Math.floor(FunctionUtils.toDouble(evaluatedArgs[0]))); + return Double.valueOf(Math.floor(FunctionUtils.toDouble(evaluatedArgs[0]))); } } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathIndexOfFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathIndexOfFunc.java index 047650dc1c..00b30e06ec 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathIndexOfFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathIndexOfFunc.java @@ -32,7 +32,7 @@ public Object evalBody(DataInstance model, EvaluationContext evalContext, Object for(int i = 0 ; i < argList.length ; ++i) { if(argList[i].equals(indexedItem)) { - return new Double(i); + return Double.valueOf(i); } } return ""; diff --git a/src/main/java/org/javarosa/xpath/expr/XPathNumericLiteral.java b/src/main/java/org/javarosa/xpath/expr/XPathNumericLiteral.java index 4e5f71bba8..dc870a100c 100755 --- a/src/main/java/org/javarosa/xpath/expr/XPathNumericLiteral.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathNumericLiteral.java @@ -25,7 +25,7 @@ public XPathNumericLiteral(Double d) { @Override protected Object evalRaw(DataInstance model, EvaluationContext evalContext) { - return new Double(d); + return Double.valueOf(d); } @Override @@ -45,7 +45,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return (new Long(Double.doubleToLongBits(d))).hashCode(); + return (Long.valueOf(Double.doubleToLongBits(d))).hashCode(); } @Override diff --git a/src/main/java/org/javarosa/xpath/expr/XPathPositionFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathPositionFunc.java index e4632fc2b3..1df3606684 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathPositionFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathPositionFunc.java @@ -44,14 +44,14 @@ public Object evalBody(DataInstance model, EvaluationContext evalContext, Object } } } else if (evalContext.getContextPosition() != -1) { - return new Double(evalContext.getContextPosition()); + return Double.valueOf(evalContext.getContextPosition()); } else { return position(evalContext.getContextRef()); } } private static Double position(TreeReference refAt) { - return new Double(refAt.getMultLast()); + return Double.valueOf(refAt.getMultLast()); } } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathRandomFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathRandomFunc.java index 2049bef21b..e9782b3638 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathRandomFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathRandomFunc.java @@ -22,7 +22,7 @@ public XPathRandomFunc(XPathExpression[] args) throws XPathSyntaxException { @Override public Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) { //calculated expressions may be recomputed w/o warning! use with caution!! - return new Double(MathUtils.getRand().nextDouble()); + return Double.valueOf(MathUtils.getRand().nextDouble()); } @Override diff --git a/src/main/java/org/javarosa/xpath/expr/XPathRoundFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathRoundFunc.java index fae5ac46fc..9150e01aad 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathRoundFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathRoundFunc.java @@ -19,7 +19,7 @@ public XPathRoundFunc(XPathExpression[] args) throws XPathSyntaxException { @Override public Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) { - return new Double(Math.floor(FunctionUtils.toDouble(evaluatedArgs[0]) + 0.5)); + return Double.valueOf(Math.floor(FunctionUtils.toDouble(evaluatedArgs[0]) + 0.5)); } } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathStringLengthFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathStringLengthFunc.java index d23e9adaa3..56e4385f3b 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathStringLengthFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathStringLengthFunc.java @@ -21,9 +21,9 @@ public XPathStringLengthFunc(XPathExpression[] args) throws XPathSyntaxException public Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) { String s = FunctionUtils.toString(evaluatedArgs[0]); if (s == null) { - return new Double(0.0); + return Double.valueOf(0.0); } - return new Double(s.length()); + return Double.valueOf(s.length()); } } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathTranslateFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathTranslateFunc.java index 499f058c0d..2f182efb96 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathTranslateFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathTranslateFunc.java @@ -45,15 +45,15 @@ private static String translate(Object o1, Object o2, Object o3) { Hashtable map = new Hashtable<>(); for (int i = 0; i < Math.min(from.length(), to.length()); i++) { - if (!map.containsKey(new Character(from.charAt(i)))) { - map.put(new Character(from.charAt(i)), new Character(to.charAt(i))); + if (!map.containsKey(Character.valueOf(from.charAt(i)))) { + map.put(Character.valueOf(from.charAt(i)), Character.valueOf(to.charAt(i))); } } String toDelete = from.substring(Math.min(from.length(), to.length())); String returnValue = ""; for (int i = 0; i < source.length(); i++) { - Character current = new Character(source.charAt(i)); + Character current = Character.valueOf(source.charAt(i)); if (toDelete.indexOf(current) == -1) { if (map.containsKey(current)) { current = map.get(current); diff --git a/src/test/java/org/javarosa/core/model/data/test/IntegerDataTests.java b/src/test/java/org/javarosa/core/model/data/test/IntegerDataTests.java index 81838596a3..35d857f7c9 100755 --- a/src/test/java/org/javarosa/core/model/data/test/IntegerDataTests.java +++ b/src/test/java/org/javarosa/core/model/data/test/IntegerDataTests.java @@ -13,8 +13,8 @@ public class IntegerDataTests { @BeforeClass public static void setUp() { - one = new Integer(1); - two = new Integer(2); + one = Integer.valueOf(1); + two = Integer.valueOf(2); } @Test diff --git a/src/test/java/org/javarosa/core/model/test/ConstraintTest.java b/src/test/java/org/javarosa/core/model/test/ConstraintTest.java index 96c965e922..c240ea7ba6 100644 --- a/src/test/java/org/javarosa/core/model/test/ConstraintTest.java +++ b/src/test/java/org/javarosa/core/model/test/ConstraintTest.java @@ -50,9 +50,9 @@ public void testAnswerQuestion() { } if (q.getTextID().equals("constraint-max-label")){ - assertConstraintMaxMin(new Integer(30), null); + assertConstraintMaxMin(Integer.valueOf(30), null); } else if (q.getTextID().equals("constraint-min-label")){ - assertConstraintMaxMin(null, new Integer(10)); + assertConstraintMaxMin(null, Integer.valueOf(10)); } else if (q.getTextID().equals("constraint-max-or-min-label")){ assertUnpivotable(); } else if (q.getTextID().equals("constraint-max-and-min-label")){ diff --git a/src/test/java/org/javarosa/core/util/test/ExternalizableTest.java b/src/test/java/org/javarosa/core/util/test/ExternalizableTest.java index dc970701ca..4a77449526 100755 --- a/src/test/java/org/javarosa/core/util/test/ExternalizableTest.java +++ b/src/test/java/org/javarosa/core/util/test/ExternalizableTest.java @@ -134,23 +134,23 @@ public void doTests() { PrototypeFactory.setStaticHasher(new ClassNameHasher()); testExternalizable("string", String.class); - testExternalizable(new Byte((byte)0), Byte.class); - testExternalizable(new Byte((byte)0x55), Byte.class); - testExternalizable(new Byte((byte)0xe9), Byte.class); - testExternalizable(new Short((short)0), Short.class); - testExternalizable(new Short((short)-12345), Short.class); - testExternalizable(new Short((short)12345), Short.class); - testExternalizable(new Integer(0), Integer.class); - testExternalizable(new Integer(1234567890), Integer.class); - testExternalizable(new Integer(-1234567890), Integer.class); - testExternalizable(new Long(0), Long.class); - testExternalizable(new Long(1234567890123456789L), Long.class); - testExternalizable(new Long(-1234567890123456789L), Long.class); + testExternalizable(Byte.valueOf((byte)0), Byte.class); + testExternalizable(Byte.valueOf((byte)0x55), Byte.class); + testExternalizable(Byte.valueOf((byte)0xe9), Byte.class); + testExternalizable(Short.valueOf((short)0), Short.class); + testExternalizable(Short.valueOf((short)-12345), Short.class); + testExternalizable(Short.valueOf((short)12345), Short.class); + testExternalizable(Integer.valueOf(0), Integer.class); + testExternalizable(Integer.valueOf(1234567890), Integer.class); + testExternalizable(Integer.valueOf(-1234567890), Integer.class); + testExternalizable(Long.valueOf(0), Long.class); + testExternalizable(Long.valueOf(1234567890123456789L), Long.class); + testExternalizable(Long.valueOf(-1234567890123456789L), Long.class); testExternalizable(Boolean.TRUE, Boolean.class); testExternalizable(Boolean.FALSE, Boolean.class); - testExternalizable(new Character('e'), Character.class); - testExternalizable(new Float(123.45e6), Float.class); - testExternalizable(new Double(123.45e6), Double.class); + testExternalizable(Character.valueOf('e'), Character.class); + testExternalizable(Float.valueOf((float)123.45e6), Float.class); + testExternalizable(Double.valueOf(123.45e6), Double.class); testExternalizable(new Date(), Date.class); testExternalizable(new SampleExtz("your", "mom"), SampleExtz.class); @@ -162,16 +162,16 @@ public void doTests() { testExternalizable(new ExtWrapNullable((String)null), new ExtWrapNullable(String.class)); testExternalizable(new ExtWrapNullable("string"), new ExtWrapNullable(String.class)); testExternalizable(new ExtWrapNullable((Integer)null), new ExtWrapNullable(Integer.class)); - testExternalizable(new ExtWrapNullable(new Integer(17)), new ExtWrapNullable(Integer.class)); + testExternalizable(new ExtWrapNullable(Integer.valueOf(17)), new ExtWrapNullable(Integer.class)); testExternalizable(new ExtWrapNullable((SampleExtz)null), new ExtWrapNullable(SampleExtz.class)); testExternalizable(new ExtWrapNullable(new SampleExtz("hi", "there")), new ExtWrapNullable(SampleExtz.class)); //vectors of base types Vector v = new Vector(); - v.addElement(new Integer(27)); - v.addElement(new Integer(-73)); - v.addElement(new Integer(1024)); - v.addElement(new Integer(66066066)); + v.addElement(Integer.valueOf(27)); + v.addElement(Integer.valueOf(-73)); + v.addElement(Integer.valueOf(1024)); + v.addElement(Integer.valueOf(66066066)); testExternalizable(new ExtWrapList(v), new ExtWrapList(Integer.class)); Vector vs = new Vector(); @@ -196,8 +196,8 @@ public void doTests() { //vectors of vectors (including empties) Vector x = new Vector(); - x.addElement(new Integer(-35)); - x.addElement(new Integer(-31415926)); + x.addElement(Integer.valueOf(-35)); + x.addElement(Integer.valueOf(-31415926)); Vector y = new Vector(); y.addElement(v); y.addElement(x); @@ -207,7 +207,7 @@ public void doTests() { //tagged base types testExternalizable(new ExtWrapTagged("string"), new ExtWrapTagged()); - testExternalizable(new ExtWrapTagged(new Integer(5000)), new ExtWrapTagged()); + testExternalizable(new ExtWrapTagged(Integer.valueOf(5000)), new ExtWrapTagged()); //tagged custom type //PrototypeFactory pf = new PrototypeFactory(); pf.addClass(SampleExtz.class); @@ -223,7 +223,7 @@ public void doTests() { //polymorphic vectors Vector a = new Vector(); - a.addElement(new Integer(47)); + a.addElement(Integer.valueOf(47)); a.addElement("string"); a.addElement(Boolean.FALSE); a.addElement(new SampleExtz("hello", "dolly")); @@ -258,8 +258,8 @@ public void doTests() { testExternalizable(new ExtWrapTagged(new ExtWrapMap(h)), new ExtWrapTagged(), pf); Hashtable j = new Hashtable(); - j.put(new Integer(17), h); - j.put(new Integer(-3), h); + j.put(Integer.valueOf(17), h); + j.put(Integer.valueOf(-3), h); Hashtable k = new Hashtable(); k.put("key", j); testExternalizable(new ExtWrapMap(k, new ExtWrapMap(Integer.class, new ExtWrapMap(String.class, SampleExtz.class))), @@ -267,8 +267,8 @@ public void doTests() { OrderedHashtable m = new OrderedHashtable(); m.put("a", "b"); - m.put("b", new Integer(17)); - m.put("c", new Short((short)-443)); + m.put("b", Integer.valueOf(17)); + m.put("c", Short.valueOf((short)-443)); m.put("d", new SampleExtz("boris", "yeltsin")); m.put("e", new ExtWrapList(vs)); testExternalizable(new ExtWrapMapPoly(m), new ExtWrapMapPoly(String.class, true), pf); diff --git a/src/test/java/org/javarosa/core/util/test/NumericEncodingTest.java b/src/test/java/org/javarosa/core/util/test/NumericEncodingTest.java index 962d9e94fd..cc42463fbd 100755 --- a/src/test/java/org/javarosa/core/util/test/NumericEncodingTest.java +++ b/src/test/java/org/javarosa/core/util/test/NumericEncodingTest.java @@ -62,7 +62,7 @@ public void testIntEncodingSmall() { } public void testNumericEncoding(long val, ExtWrapIntEncoding encoding) { - ExternalizableTest.testExternalizable(encoding.clone(new Long(val)), encoding, null, null); + ExternalizableTest.testExternalizable(encoding.clone(Long.valueOf(val)), encoding, null, null); } } diff --git a/src/test/java/org/javarosa/xform/util/test/XFormAnswerDataSerializerTest.java b/src/test/java/org/javarosa/xform/util/test/XFormAnswerDataSerializerTest.java index 553f74feb9..961897fd93 100755 --- a/src/test/java/org/javarosa/xform/util/test/XFormAnswerDataSerializerTest.java +++ b/src/test/java/org/javarosa/xform/util/test/XFormAnswerDataSerializerTest.java @@ -23,7 +23,7 @@ */ public class XFormAnswerDataSerializerTest { static final String stringDataValue = "String Data Value"; - static final Integer integerDataValue = new Integer(5); + static final Integer integerDataValue = Integer.valueOf(5); static final Date dateDataValue = new Date(); static final Date timeDataValue = new Date(); diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index 2ecba44f7c..24b69d2745 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -182,14 +182,14 @@ public void doTests() { testEval("(filter-expr)[5]", instance, null, new XPathUnsupportedException()); testEval("(filter-expr)/data", instance, null, new XPathUnsupportedException()); /* numeric literals */ - testEval("5", null, null, new Double(5.0)); - testEval("555555.555", null, null, new Double(555555.555)); - testEval(".000555", null, null, new Double(0.000555)); - testEval("0", null, null, new Double(0.0)); - testEval("-5", null, null, new Double(-5.0)); - testEval("-0", null, null, new Double(-0.0)); - testEval("1230000000000000000000", null, null, new Double(1.23e21)); - testEval("0.00000000000000000123", null, null, new Double(1.23e-18)); + testEval("5", null, null, Double.valueOf(5.0)); + testEval("555555.555", null, null, Double.valueOf(555555.555)); + testEval(".000555", null, null, Double.valueOf(0.000555)); + testEval("0", null, null, Double.valueOf(0.0)); + testEval("-5", null, null, Double.valueOf(-5.0)); + testEval("-0", null, null, Double.valueOf(-0.0)); + testEval("1230000000000000000000", null, null, Double.valueOf(1.23e21)); + testEval("0.00000000000000000123", null, null, Double.valueOf(1.23e-18)); /* string literals */ testEval("''", null, null, ""); testEval("'\"'", null, null, "\""); @@ -215,48 +215,48 @@ public void doTests() { testEval("boolean(date('2000-01-01'))", null, null, Boolean.TRUE); testEval("boolean(convertible())", null, ec, Boolean.TRUE); testEval("boolean(inconvertible())", null, ec, new XPathTypeMismatchException()); - testEval("number(true())", null, null, new Double(1.0)); - testEval("number(false())", null, null, new Double(0.0)); - testEval("number('100')", null, null, new Double(100.0)); - testEval("number('100.001')", null, null, new Double(100.001)); - testEval("number('.1001')", null, null, new Double(0.1001)); - testEval("number('1230000000000000000000')", null, null, new Double(1.23e21)); - testEval("number('0.00000000000000000123')", null, null, new Double(1.23e-18)); - testEval("number('0')", null, null, new Double(0.0)); - testEval("number('-0')", null, null, new Double(-0.0)); - testEval("number(' -12345.6789 ')", null, null, new Double(-12345.6789)); - testEval("number('NaN')", null, null, new Double(Double.NaN)); - testEval("number('not a number')", null, null, new Double(Double.NaN)); - testEval("number('- 17')", null, null, new Double(Double.NaN)); - testEval("number(' ')", null, null, new Double(Double.NaN)); - testEval("number('')", null, null, new Double(Double.NaN)); - testEval("number('Infinity')", null, null, new Double(Double.NaN)); - testEval("number('1.1e6')", null, null, new Double(Double.NaN)); - testEval("number('34.56.7')", null, null, new Double(Double.NaN)); - testEval("number(10)", null, null, new Double(10.0)); - testEval("number(0)", null, null, new Double(0.0)); - testEval("number(-0)", null, null, new Double(-0.0)); - testEval("number(-123.5)", null, null, new Double(-123.5)); - testEval("number(number('NaN'))", null, null, new Double(Double.NaN)); - testEval("number(1 div 0)", null, null, new Double(Double.POSITIVE_INFINITY)); - testEval("number(-1 div 0)", null, null, new Double(Double.NEGATIVE_INFINITY)); - testEval("number(date('1970-01-01'))", null, null, new Double(0.0)); - testEval("number(date('1970-01-02'))", null, null, new Double(1.0)); - testEval("number(date('1969-12-31'))", null, null, new Double(-1.0)); - testEval("number(date('2008-09-05'))", null, null, new Double(14127.0)); - testEval("number(date('1941-12-07'))", null, null, new Double(-10252.0)); - testEval("number('1970-01-01')", null, null, new Double(0.0)); - testEval("number('1970-01-02')", null, null, new Double(1.0)); - testEval("number('1969-12-31')", null, null, new Double(-1.0)); - testEval("number('2008-09-05')", null, null, new Double(14127.0)); - testEval("number('1941-12-07')", null, null, new Double(-10252.0)); - testEval("number('1970-01')", null, null, new Double(Double.NaN)); - testEval("number('-1970-01-02')", null, null, new Double(Double.NaN)); - testEval("number('12-31')", null, null, new Double(Double.NaN)); - testEval("number('2016-13-13')", null, null, new Double(Double.NaN)); - testEval("number('2017-01-45')", null, null, new Double(Double.NaN)); - - testEval("number(convertible())", null, ec, new Double(5.0)); + testEval("number(true())", null, null, Double.valueOf(1.0)); + testEval("number(false())", null, null, Double.valueOf(0.0)); + testEval("number('100')", null, null, Double.valueOf(100.0)); + testEval("number('100.001')", null, null, Double.valueOf(100.001)); + testEval("number('.1001')", null, null, Double.valueOf(0.1001)); + testEval("number('1230000000000000000000')", null, null, Double.valueOf(1.23e21)); + testEval("number('0.00000000000000000123')", null, null, Double.valueOf(1.23e-18)); + testEval("number('0')", null, null, Double.valueOf(0.0)); + testEval("number('-0')", null, null, Double.valueOf(-0.0)); + testEval("number(' -12345.6789 ')", null, null, Double.valueOf(-12345.6789)); + testEval("number('NaN')", null, null, Double.valueOf(Double.NaN)); + testEval("number('not a number')", null, null, Double.valueOf(Double.NaN)); + testEval("number('- 17')", null, null, Double.valueOf(Double.NaN)); + testEval("number(' ')", null, null, Double.valueOf(Double.NaN)); + testEval("number('')", null, null, Double.valueOf(Double.NaN)); + testEval("number('Infinity')", null, null, Double.valueOf(Double.NaN)); + testEval("number('1.1e6')", null, null, Double.valueOf(Double.NaN)); + testEval("number('34.56.7')", null, null, Double.valueOf(Double.NaN)); + testEval("number(10)", null, null, Double.valueOf(10.0)); + testEval("number(0)", null, null, Double.valueOf(0.0)); + testEval("number(-0)", null, null, Double.valueOf(-0.0)); + testEval("number(-123.5)", null, null, Double.valueOf(-123.5)); + testEval("number(number('NaN'))", null, null, Double.valueOf(Double.NaN)); + testEval("number(1 div 0)", null, null, Double.valueOf(Double.POSITIVE_INFINITY)); + testEval("number(-1 div 0)", null, null, Double.valueOf(Double.NEGATIVE_INFINITY)); + testEval("number(date('1970-01-01'))", null, null, Double.valueOf(0.0)); + testEval("number(date('1970-01-02'))", null, null, Double.valueOf(1.0)); + testEval("number(date('1969-12-31'))", null, null, Double.valueOf(-1.0)); + testEval("number(date('2008-09-05'))", null, null, Double.valueOf(14127.0)); + testEval("number(date('1941-12-07'))", null, null, Double.valueOf(-10252.0)); + testEval("number('1970-01-01')", null, null, Double.valueOf(0.0)); + testEval("number('1970-01-02')", null, null, Double.valueOf(1.0)); + testEval("number('1969-12-31')", null, null, Double.valueOf(-1.0)); + testEval("number('2008-09-05')", null, null, Double.valueOf(14127.0)); + testEval("number('1941-12-07')", null, null, Double.valueOf(-10252.0)); + testEval("number('1970-01')", null, null, Double.valueOf(Double.NaN)); + testEval("number('-1970-01-02')", null, null, Double.valueOf(Double.NaN)); + testEval("number('12-31')", null, null, Double.valueOf(Double.NaN)); + testEval("number('2016-13-13')", null, null, Double.valueOf(Double.NaN)); + testEval("number('2017-01-45')", null, null, Double.valueOf(Double.NaN)); + + testEval("number(convertible())", null, ec, Double.valueOf(5.0)); testEval("number(inconvertible())", null, ec, new XPathTypeMismatchException()); testEval("string(true())", null, null, "true"); testEval("string(false())", null, null, "false"); @@ -328,7 +328,7 @@ public void doTests() { testEval("boolean-from-string(1.0001)", null, null, Boolean.FALSE); testEval("boolean-from-string(true())", null, null, Boolean.TRUE); testEval("if(true())", null, null, new XPathSyntaxException()); - testEval("if(true(), 5, 'abc')", null, null, new Double(5.0)); + testEval("if(true(), 5, 'abc')", null, null, Double.valueOf(5.0)); testEval("if(false(), 5, 'abc')", null, null, "abc"); testEval("if(6 > 7, 5, 'abc')", null, null, "abc"); testEval("if('', 5, 'abc')", null, null, "abc"); @@ -343,61 +343,61 @@ public void doTests() { testEval("selected('', 'apple')", null, null, Boolean.FALSE); /* operators */ - testEval("min(5.5, 0.5)", null, null, new Double(0.5)); - testEval("min(5.5)", null, null, new Double(5.5)); - testEval("min(-2,-3)", null, null, new Double(-3)); - testEval("min(2,-3)", null, null, new Double(-3)); + testEval("min(5.5, 0.5)", null, null, Double.valueOf(0.5)); + testEval("min(5.5)", null, null, Double.valueOf(5.5)); + testEval("min(-2,-3)", null, null, Double.valueOf(-3)); + testEval("min(2,-3)", null, null, Double.valueOf(-3)); testEval("date(min(date('2012-02-05'), date('2012-01-01')))", null, null, DateUtils.parseDate("2012-01-01")); - testEval("max(5.5, 0.5)", null, null, new Double(5.5)); - testEval("max(0.5)", null, null, new Double(0.5)); - testEval("max(-2,-3)", null, null, new Double(-2)); - testEval("max(2,-3)", null, null, new Double(2)); + testEval("max(5.5, 0.5)", null, null, Double.valueOf(5.5)); + testEval("max(0.5)", null, null, Double.valueOf(0.5)); + testEval("max(-2,-3)", null, null, Double.valueOf(-2)); + testEval("max(2,-3)", null, null, Double.valueOf(2)); testEval("date(max(date('2012-02-05'), date('2012-01-01')))", null, null, DateUtils.parseDate("2012-02-05")); // Test that taking the min or max of date-strings works, but still fails properly for // numeric strings that are not dates testEval("min('2012-02-05', '2012-01-01', '2012-04-20')", null, null, - new Double(DateUtils.daysSinceEpoch(DateUtils.parseDate("2012-01-01")))); + Double.valueOf(DateUtils.daysSinceEpoch(DateUtils.parseDate("2012-01-01")))); testEval("max('2012-02-05', '2012-01-01', '2012-04-20')", null, null, - new Double(DateUtils.daysSinceEpoch(DateUtils.parseDate("2012-04-20")))); + Double.valueOf(DateUtils.daysSinceEpoch(DateUtils.parseDate("2012-04-20")))); testEval("max('-1-02-05', '2012-01-01', '2012-04-20')", null, null, - new Double(Double.NaN)); + Double.valueOf(Double.NaN)); testEval("max('02-05', '2012-01-01', '2012-04-20')", null, null, - new Double(Double.NaN)); + Double.valueOf(Double.NaN)); testEval("max('2012-14-05', '2012-01-01', '2012-04-20')", null, null, - new Double(Double.NaN)); - - - testEval("5.5 + 5.5", null, null, new Double(11.0)); - testEval("0 + 0", null, null, new Double(0.0)); - testEval("6.1 - 7.8", null, null, new Double(-1.7)); - testEval("-3 + 4", null, null, new Double(1.0)); - testEval("3 + -4", null, null, new Double(-1.0)); - testEval("1 - 2 - 3", null, null, new Double(-4.0)); - testEval("1 - (2 - 3)", null, null, new Double(2.0)); - testEval("-(8*5)", null, null, new Double(-40.0)); - testEval("-'19'", null, null, new Double(-19.0)); - testEval("1.1 * -1.1", null, null, new Double(-1.21)); - testEval("-10 div -4", null, null, new Double(2.5)); - testEval("2 * 3 div 8 * 2", null, null, new Double(1.5)); - testEval("3 + 3 * 3", null, null, new Double(12.0)); - testEval("1 div 0", null, null, new Double(Double.POSITIVE_INFINITY)); - testEval("-1 div 0", null, null, new Double(Double.NEGATIVE_INFINITY)); - testEval("0 div 0", null, null, new Double(Double.NaN)); - testEval("3.1 mod 3.1", null, null, new Double(0.0)); - testEval("5 mod 3.1", null, null, new Double(1.9)); - testEval("2 mod 3.1", null, null, new Double(2.0)); - testEval("0 mod 3.1", null, null, new Double(0.0)); - testEval("5 mod -3", null, null, new Double(2.0)); - testEval("-5 mod 3", null, null, new Double(-2.0)); - testEval("-5 mod -3", null, null, new Double(-2.0)); - testEval("5 mod 0", null, null, new Double(Double.NaN)); - testEval("5 * (6 + 7)", null, null, new Double(65.0)); - testEval("'123' * '456'", null, null, new Double(56088.0)); - testEval("true() + 8", null, null, new Double(9.0)); - testEval("date('2008-09-08') - date('1983-10-06')", null, null, new Double(9104.0)); + Double.valueOf(Double.NaN)); + + + testEval("5.5 + 5.5", null, null, Double.valueOf(11.0)); + testEval("0 + 0", null, null, Double.valueOf(0.0)); + testEval("6.1 - 7.8", null, null, Double.valueOf(-1.7)); + testEval("-3 + 4", null, null, Double.valueOf(1.0)); + testEval("3 + -4", null, null, Double.valueOf(-1.0)); + testEval("1 - 2 - 3", null, null, Double.valueOf(-4.0)); + testEval("1 - (2 - 3)", null, null, Double.valueOf(2.0)); + testEval("-(8*5)", null, null, Double.valueOf(-40.0)); + testEval("-'19'", null, null, Double.valueOf(-19.0)); + testEval("1.1 * -1.1", null, null, Double.valueOf(-1.21)); + testEval("-10 div -4", null, null, Double.valueOf(2.5)); + testEval("2 * 3 div 8 * 2", null, null, Double.valueOf(1.5)); + testEval("3 + 3 * 3", null, null, Double.valueOf(12.0)); + testEval("1 div 0", null, null, Double.valueOf(Double.POSITIVE_INFINITY)); + testEval("-1 div 0", null, null, Double.valueOf(Double.NEGATIVE_INFINITY)); + testEval("0 div 0", null, null, Double.valueOf(Double.NaN)); + testEval("3.1 mod 3.1", null, null, Double.valueOf(0.0)); + testEval("5 mod 3.1", null, null, Double.valueOf(1.9)); + testEval("2 mod 3.1", null, null, Double.valueOf(2.0)); + testEval("0 mod 3.1", null, null, Double.valueOf(0.0)); + testEval("5 mod -3", null, null, Double.valueOf(2.0)); + testEval("-5 mod 3", null, null, Double.valueOf(-2.0)); + testEval("-5 mod -3", null, null, Double.valueOf(-2.0)); + testEval("5 mod 0", null, null, Double.valueOf(Double.NaN)); + testEval("5 * (6 + 7)", null, null, Double.valueOf(65.0)); + testEval("'123' * '456'", null, null, Double.valueOf(56088.0)); + testEval("true() + 8", null, null, Double.valueOf(9.0)); + testEval("date('2008-09-08') - date('1983-10-06')", null, null, Double.valueOf(9104.0)); testEval("true() and true()", null, null, Boolean.TRUE); testEval("true() and false()", null, null, Boolean.FALSE); testEval("false() and false()", null, null, Boolean.FALSE); @@ -410,7 +410,7 @@ public void doTests() { testEval("false() and date('')", null, null, Boolean.FALSE); //short-circuiting testEval("'' or 17", null, null, Boolean.TRUE); testEval("false() or 0 + 2", null, null, Boolean.TRUE); - testEval("(false() or 0) + 2", null, null, new Double(2.0)); + testEval("(false() or 0) + 2", null, null, Double.valueOf(2.0)); testEval("4 < 5", null, null, Boolean.TRUE); testEval("5 < 5", null, null, Boolean.FALSE); testEval("6 < 5", null, null, Boolean.FALSE); @@ -439,30 +439,30 @@ public void doTests() { testEval("(double(date('2004-05-01T07:00:00')) - double(date('2004-05-01T00:30:00'))) > (6.0 div 24) ", null, null, Boolean.TRUE); testEval("(double(date('2004-05-03T07:00:00')) - double(date('2004-05-01T03:00:00'))) > (6.0 div 24) ", null, null, Boolean.TRUE); - testEval("abs(-3.5)", null, null, new Double(3.5)); - testEval("abs(2)", null, null, new Double(2.0)); - testEval("floor(-4.8)", null, null, new Double(-5.0)); - testEval("floor(100.2)", null, null, new Double(100.0)); - testEval("ceiling(-0.5)", null, null, new Double(0.0)); - testEval("ceiling(10.4)", null, null, new Double(11.0)); - testEval("round(1.5)", null, null, new Double(2.0)); - testEval("round(-1.5)", null, null, new Double(-1.0)); - testEval("round(1.455)", null, null, new Double(1.0)); - - testEval("log(" + Math.E + ")", null, null, new Double(1.0)); - testEval("log(1)", null, null, new Double(0.0)); - testEval("log10(100)", null, null, new Double(2.0)); - testEval("log10(1)", null, null, new Double(0.0)); - - - testEval("pow(2, 2)", null, null, new Double(4.0)); - testEval("pow(2, 0)", null, null, new Double(1.0)); - testEval("pow(0, 4)", null, null, new Double(0.0)); - testEval("pow(2.5, 2)", null, null, new Double(6.25)); - testEval("pow(0.5, 2)", null, null, new Double(.25)); - - testEval("pow(-1, 2)", null, null, new Double(1.0)); - testEval("pow(-1, 3)", null, null, new Double(-1.0)); + testEval("abs(-3.5)", null, null, Double.valueOf(3.5)); + testEval("abs(2)", null, null, Double.valueOf(2.0)); + testEval("floor(-4.8)", null, null, Double.valueOf(-5.0)); + testEval("floor(100.2)", null, null, Double.valueOf(100.0)); + testEval("ceiling(-0.5)", null, null, Double.valueOf(0.0)); + testEval("ceiling(10.4)", null, null, Double.valueOf(11.0)); + testEval("round(1.5)", null, null, Double.valueOf(2.0)); + testEval("round(-1.5)", null, null, Double.valueOf(-1.0)); + testEval("round(1.455)", null, null, Double.valueOf(1.0)); + + testEval("log(" + Math.E + ")", null, null, Double.valueOf(1.0)); + testEval("log(1)", null, null, Double.valueOf(0.0)); + testEval("log10(100)", null, null, Double.valueOf(2.0)); + testEval("log10(1)", null, null, Double.valueOf(0.0)); + + + testEval("pow(2, 2)", null, null, Double.valueOf(4.0)); + testEval("pow(2, 0)", null, null, Double.valueOf(1.0)); + testEval("pow(0, 4)", null, null, Double.valueOf(0.0)); + testEval("pow(2.5, 2)", null, null, Double.valueOf(6.25)); + testEval("pow(0.5, 2)", null, null, Double.valueOf(.25)); + + testEval("pow(-1, 2)", null, null, Double.valueOf(1.0)); + testEval("pow(-1, 3)", null, null, Double.valueOf(-1.0)); testEval("sin(0)", null, null, 0.0); testEval("cos(0)", null, null, 1.0); testEval("tan(0)", null, null, 0.0); @@ -477,15 +477,15 @@ public void doTests() { //So raising things to decimal powers is.... very hard //to evaluated exactly due to double floating point //precision. We'll try for things with clean answers - //testEval("pow(4, 0.5)", null, null, new Double(2.0), .001); - //testEval("pow(16, 0.25)", null, null, new Double(2.0), .001); + //testEval("pow(4, 0.5)", null, null, Double.valueOf(2.0), .001); + //testEval("pow(16, 0.25)", null, null, Double.valueOf(2.0), .001); //CTS: We're going to skip trying to do any sort of hackery workaround //for this for now and go with "Integer powers only" testEval("false() and false() < true()", null, null, Boolean.FALSE); testEval("(false() and false()) < true()", null, null, Boolean.TRUE); testEval("6 < 7 - 4", null, null, Boolean.FALSE); - testEval("(6 < 7) - 4", null, null, new Double(-3.0)); + testEval("(6 < 7) - 4", null, null, Double.valueOf(-3.0)); testEval("3 < 4 < 5", null, null, Boolean.TRUE); testEval("3 < (4 < 5)", null, null, Boolean.FALSE); testEval("true() = true()", null, null, Boolean.TRUE); @@ -536,8 +536,8 @@ public void doTests() { testEval("string('too', 'many', 'args')", null, null, new XPathArityException()); testEval("not-a-function()", null, null, new XPathUnhandledException()); testEval("testfunc()", null, ec, Boolean.TRUE); - testEval("add(3, 5)", null, ec, new Double(8.0)); - testEval("add('17', '-14')", null, ec, new Double(3.0)); + testEval("add(3, 5)", null, ec, Double.valueOf(8.0)); + testEval("add('17', '-14')", null, ec, Double.valueOf(3.0)); // proto not setup for 0 arguments. Note that Arity is a parse exception, so we expect this // to get wrapped testEval("proto()", null, ec, new XPathTypeMismatchException()); @@ -605,10 +605,10 @@ public void doTests() { //Variables EvaluationContext varContext = getVariableContext(); - testEval("$var_float_five", null, varContext, new Double(5.0)); + testEval("$var_float_five", null, varContext, Double.valueOf(5.0)); testEval("$var_string_five", null, varContext, "five"); - testEval("$var_int_five", null, varContext, new Double(5.0)); - testEval("$var_double_five", null, varContext, new Double(5.0)); + testEval("$var_int_five", null, varContext, Double.valueOf(5.0)); + testEval("$var_double_five", null, varContext, Double.valueOf(5.0)); //Attribute XPath References //testEval("/@blah", null, null, new XPathUnsupportedException()); @@ -667,17 +667,17 @@ public void doTests() { addDataRef(instance, "/data/rangetest[2]/@num", new StringData("3")); testEval("/data/string", instance, null, "string"); - testEval("/data/int", instance, null, new Double(17.0)); + testEval("/data/int", instance, null, Double.valueOf(17.0)); - testEval("min(/data/int, /data/int_two)", instance, null, new Double(5.0)); + testEval("min(/data/int, /data/int_two)", instance, null, Double.valueOf(5.0)); - testEval("count(/data/predtest[@val = /data/string_two])", instance, null, new Double(2)); - testEval("count(/data/predtest[@val = 2])", instance, null, new Double(2)); - testEval("count(/data/predtest[2 = @val])", instance, null, new Double(2)); + testEval("count(/data/predtest[@val = /data/string_two])", instance, null, Double.valueOf(2)); + testEval("count(/data/predtest[@val = 2])", instance, null, Double.valueOf(2)); + testEval("count(/data/predtest[2 = @val])", instance, null, Double.valueOf(2)); - testEval("count(/data/strtest[@val = 'a'])", instance, null, new Double(1)); - testEval("count(/data/strtest[@val = 2])", instance, null, new Double(0)); - testEval("count(/data/strtest[@val = /data/string])", instance, null, new Double(1)); + testEval("count(/data/strtest[@val = 'a'])", instance, null, Double.valueOf(1)); + testEval("count(/data/strtest[@val = 2])", instance, null, Double.valueOf(0)); + testEval("count(/data/strtest[@val = /data/string])", instance, null, Double.valueOf(1)); testEval("sum(/data/predtest/@num)", instance, null, 4.0); testEval("concat(/data/predtest/@num)", instance, null, "2.02"); @@ -695,8 +695,8 @@ public void doTests() { testEval("max(/data/rangetest[0])", instance, null, Double.NaN); testEval("min(/data/rangetest[0])", instance, null, Double.NaN); - testEval("max(/data/rangetest/@num)", instance, null, new Double("3")); - testEval("min(/data/rangetest/@num)", instance, null, new Double("-2")); + testEval("max(/data/rangetest/@num)", instance, null, Double.valueOf("3")); + testEval("min(/data/rangetest/@num)", instance, null, Double.valueOf("-2")); } private void configureLocaleForCalendar() { @@ -947,7 +947,7 @@ public boolean rawArgs() { @Override public Object eval(Object[] args, EvaluationContext ec) { - return new Double(((Double)args[0]).doubleValue() + ((Double)args[1]).doubleValue()); + return Double.valueOf(((Double)args[0]).doubleValue() + ((Double)args[1]).doubleValue()); } }); @@ -1052,7 +1052,7 @@ public Boolean toBoolean() { @Override public Double toNumeric() { - return new Double(5.0); + return Double.valueOf(5.0); } @Override @@ -1144,10 +1144,10 @@ public Object eval(Object[] args, EvaluationContext ec) { private EvaluationContext getVariableContext() { EvaluationContext ec = new EvaluationContext(null); - ec.setVariable("var_float_five", new Float(5.0)); + ec.setVariable("var_float_five", Float.valueOf((float)5.0)); ec.setVariable("var_string_five", "five"); - ec.setVariable("var_int_five", new Integer(5)); - ec.setVariable("var_double_five", new Double(5.0)); + ec.setVariable("var_int_five", Integer.valueOf(5)); + ec.setVariable("var_double_five", Double.valueOf(5.0)); return ec; } diff --git a/src/translate/java/com/csvreader/CsvReader.java b/src/translate/java/com/csvreader/CsvReader.java index 87b858d15a..97dc07e317 100644 --- a/src/translate/java/com/csvreader/CsvReader.java +++ b/src/translate/java/com/csvreader/CsvReader.java @@ -506,7 +506,7 @@ public void setHeaders(String[] headers) { // use headersHolder.Length here in case headers is null for (int i = 0; i < headersHolder.Length; i++) { - headersHolder.IndexByName.put(headers[i], new Integer(i)); + headersHolder.IndexByName.put(headers[i], Integer.valueOf(i)); } } @@ -1237,7 +1237,7 @@ public boolean readHeaders() throws IOException { headersHolder.Headers[i] = columnValue; // if there are duplicate header names, we will save the last one - headersHolder.IndexByName.put(columnValue, new Integer(i)); + headersHolder.IndexByName.put(columnValue, Integer.valueOf(i)); } if (result) { From a21d4ed49f4653a325ec701f970023b2e83f713c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Dec 2022 00:28:00 +0000 Subject: [PATCH 020/402] Bump hppc from 0.7.2 to 0.9.1 Bumps [hppc](https://github.com/carrotsearch/hppc) from 0.7.2 to 0.9.1. - [Release notes](https://github.com/carrotsearch/hppc/releases) - [Changelog](https://github.com/carrotsearch/hppc/blob/master/CHANGES.txt) - [Commits](https://github.com/carrotsearch/hppc/compare/0.7.2...0.9.1) --- updated-dependencies: - dependency-name: com.carrotsearch:hppc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d72b460716..4dc65af933 100644 --- a/build.gradle +++ b/build.gradle @@ -74,7 +74,7 @@ dependencies { implementation 'org.json:json:20220924' implementation 'commons-cli:commons-cli:1.3.1' implementation 'joda-time:joda-time:2.12.1' - implementation 'com.carrotsearch:hppc:0.7.2' + implementation 'com.carrotsearch:hppc:0.9.1' api 'com.squareup.retrofit2:retrofit:2.3.0' api 'com.squareup.okhttp3:okhttp:3.12.12' implementation 'com.google.code.findbugs:jsr305:3.0.2' From 0de1350c9eead9843e0494fe0e1782e14f512b52 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 20 Jan 2023 13:41:44 +0100 Subject: [PATCH 021/402] supporting methods for instance reference parsing --- .../commcare/data/xml/VirtualInstances.java | 30 +++++++++++++++-- .../data/xml/VirtualInstancesTest.java | 33 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/commcare/data/xml/VirtualInstances.java b/src/main/java/org/commcare/data/xml/VirtualInstances.java index 8ab9680a82..438ca5df74 100644 --- a/src/main/java/org/commcare/data/xml/VirtualInstances.java +++ b/src/main/java/org/commcare/data/xml/VirtualInstances.java @@ -51,18 +51,42 @@ public static ExternalDataInstance buildSelectedValuesInstance( public static String getSelectedEntitiesReference(String referenceId) { - return JR_SELECTED_ENTITIES_REFERENCE.concat("/").concat(referenceId); + return getInstanceReference(JR_SELECTED_ENTITIES_REFERENCE, referenceId); } public static String getSearchInputReference(String referenceId) { - return JR_SEARCH_INPUT_REFERENCE.concat("/").concat(referenceId); + return getInstanceReference(JR_SEARCH_INPUT_REFERENCE, referenceId); } public static String getRemoteReference(String referenceId) { - return JR_REMOTE_REFERENCE.concat("/").concat(referenceId); + return getInstanceReference(JR_REMOTE_REFERENCE, referenceId); } + /** + * Parses instance reference of format "refScheme/refId" to return the reference id + * @param reference An instance reference in form of "refScheme/refId" + * @return reference id from the given reference + */ public static String getReferenceId(String reference) { return reference.substring(reference.lastIndexOf('/') + 1); } + + /** + * Parses instance reference of format "refScheme/refId" to return the reference scheme + * @param reference An instance reference in form of "refScheme/refId" + * @return reference scheme from the given reference + */ + public static String getReferenceScheme(String reference) { + return reference.substring(0, reference.lastIndexOf('/')); + } + + /** + * Constructs an instance reference in format "refScheme/refId" + * @param referenceScheme reference scheme for the instance reference + * @param referenceId reference id for the instance reference + * @return an instance reference in format "refScheme/refId" + */ + public static String getInstanceReference(String referenceScheme, String referenceId) { + return referenceScheme.concat("/").concat(referenceId); + } } diff --git a/src/test/java/org/commcare/data/xml/VirtualInstancesTest.java b/src/test/java/org/commcare/data/xml/VirtualInstancesTest.java index 6effb1f469..b7b3feca1a 100644 --- a/src/test/java/org/commcare/data/xml/VirtualInstancesTest.java +++ b/src/test/java/org/commcare/data/xml/VirtualInstancesTest.java @@ -41,4 +41,37 @@ public void testBuildSearchInputRoot() ); assertEquals(expected, instance.getRoot()); } + + @Test + public void testBuildSelectedValuesInstance() + throws UnfullfilledRequirementsException, XmlPullParserException, InvalidStructureException, + IOException { + String instanceId = "selected-cases"; + String[] selectedValues = new String[]{"case1", "case2"}; + ExternalDataInstance instance = VirtualInstances.buildSelectedValuesInstance(instanceId, selectedValues); + String expectedXml = String.join( + "", + "", + "case1", + "case2", + "" + ); + TreeElement expected = ExternalDataInstance.parseExternalTree( + new ByteArrayInputStream(expectedXml.getBytes(StandardCharsets.UTF_8)), + instanceId + ); + assertEquals(expected, instance.getRoot()); + } + + @Test + public void testGetReferenceId() { + String instanceReference = VirtualInstances.getRemoteReference("instanceId"); + assertEquals("instanceId", VirtualInstances.getReferenceId(instanceReference)); + } + + @Test + public void testGetReferenceScheme() { + String instanceReference = VirtualInstances.getRemoteReference("instanceId"); + assertEquals(ExternalDataInstance.JR_REMOTE_REFERENCE, VirtualInstances.getReferenceScheme(instanceReference)); + } } From 0fecb01f0c57b3d858b936efde5977e0d25d1629 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 20 Jan 2023 14:09:21 +0100 Subject: [PATCH 022/402] change access for isNonUniqueReference to use in Formplayer classes --- .../org/commcare/core/process/CommCareInstanceInitializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java b/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java index 475b87688f..7c15d4644a 100644 --- a/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java +++ b/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java @@ -137,7 +137,7 @@ protected InstanceRoot setupExternalDataInstance(ExternalDataInstance instance, return instanceRoot == null ? ConcreteInstanceRoot.NULL : instanceRoot; } - private boolean isNonUniqueReference(String reference) { + public static boolean isNonUniqueReference(String reference) { return reference.contentEquals(ExternalDataInstance.JR_REMOTE_REFERENCE) || reference.contentEquals(ExternalDataInstance.JR_SELECTED_ENTITIES_REFERENCE) || reference.contentEquals(ExternalDataInstance.JR_SEARCH_INPUT_REFERENCE); From 87b3997425320a52511e9838c33982a7f5d69919 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 20 Jan 2023 14:17:46 +0100 Subject: [PATCH 023/402] stickler --- src/main/java/org/commcare/data/xml/VirtualInstances.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/data/xml/VirtualInstances.java b/src/main/java/org/commcare/data/xml/VirtualInstances.java index 438ca5df74..6427078d2e 100644 --- a/src/main/java/org/commcare/data/xml/VirtualInstances.java +++ b/src/main/java/org/commcare/data/xml/VirtualInstances.java @@ -64,6 +64,7 @@ public static String getRemoteReference(String referenceId) { /** * Parses instance reference of format "refScheme/refId" to return the reference id + * * @param reference An instance reference in form of "refScheme/refId" * @return reference id from the given reference */ @@ -73,6 +74,7 @@ public static String getReferenceId(String reference) { /** * Parses instance reference of format "refScheme/refId" to return the reference scheme + * * @param reference An instance reference in form of "refScheme/refId" * @return reference scheme from the given reference */ @@ -82,8 +84,9 @@ public static String getReferenceScheme(String reference) { /** * Constructs an instance reference in format "refScheme/refId" + * * @param referenceScheme reference scheme for the instance reference - * @param referenceId reference id for the instance reference + * @param referenceId reference id for the instance reference * @return an instance reference in format "refScheme/refId" */ public static String getInstanceReference(String referenceScheme, String referenceId) { From ed05696d1a4dd95ae98e5b479e462d8e57a580f3 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 20 Jan 2023 14:21:00 +0100 Subject: [PATCH 024/402] more lint --- .../java/org/commcare/data/xml/VirtualInstancesTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/commcare/data/xml/VirtualInstancesTest.java b/src/test/java/org/commcare/data/xml/VirtualInstancesTest.java index b7b3feca1a..1367c324fc 100644 --- a/src/test/java/org/commcare/data/xml/VirtualInstancesTest.java +++ b/src/test/java/org/commcare/data/xml/VirtualInstancesTest.java @@ -1,5 +1,7 @@ package org.commcare.data.xml; +import static org.junit.Assert.assertEquals; + import com.google.common.collect.ImmutableMap; import org.javarosa.core.model.instance.ExternalDataInstance; @@ -13,8 +15,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import static org.junit.Assert.assertEquals; - public class VirtualInstancesTest { @Test public void testBuildSearchInputRoot() @@ -72,6 +72,7 @@ public void testGetReferenceId() { @Test public void testGetReferenceScheme() { String instanceReference = VirtualInstances.getRemoteReference("instanceId"); - assertEquals(ExternalDataInstance.JR_REMOTE_REFERENCE, VirtualInstances.getReferenceScheme(instanceReference)); + assertEquals(ExternalDataInstance.JR_REMOTE_REFERENCE, + VirtualInstances.getReferenceScheme(instanceReference)); } } From b4aa1caf3eae109754e4e595eccf830314b318ad Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Wed, 11 Jan 2023 15:03:11 -0800 Subject: [PATCH 025/402] feat: parse and pulls no_items_text from suite xml --- src/main/java/org/commcare/suite/model/Detail.java | 2 +- src/main/java/org/commcare/xml/DetailParser.java | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/Detail.java b/src/main/java/org/commcare/suite/model/Detail.java index 4334efed0f..1f27092282 100755 --- a/src/main/java/org/commcare/suite/model/Detail.java +++ b/src/main/java/org/commcare/suite/model/Detail.java @@ -110,7 +110,7 @@ public Detail() { } - public Detail(String id, DisplayUnit title, String nodeset, Vector detailsVector, + public Detail(String id, DisplayUnit title, Text noItemsText, String nodeset, Vector detailsVector, Vector fieldsVector, OrderedHashtable variables, Vector actions, Callout callout, String fitAcross, String uniformUnitsString, String forceLandscape, String focusFunction, diff --git a/src/main/java/org/commcare/xml/DetailParser.java b/src/main/java/org/commcare/xml/DetailParser.java index 3dc643084d..0612e1829a 100755 --- a/src/main/java/org/commcare/xml/DetailParser.java +++ b/src/main/java/org/commcare/xml/DetailParser.java @@ -6,6 +6,7 @@ import org.commcare.suite.model.DetailField; import org.commcare.suite.model.DisplayUnit; import org.commcare.suite.model.Global; +import org.commcare.suite.model.Text; import org.javarosa.core.util.OrderedHashtable; import org.javarosa.xml.util.InvalidStructureException; import org.javarosa.xpath.XPathParseTool; @@ -55,6 +56,7 @@ public Detail parse() throws InvalidStructureException, IOException, XmlPullPars Vector actions = new Vector<>(); //Now get the headers and templates. + Text noItemsText = null; Vector subdetails = new Vector<>(); Vector fields = new Vector<>(); OrderedHashtable variables = new OrderedHashtable<>(); @@ -71,6 +73,14 @@ public Detail parse() throws InvalidStructureException, IOException, XmlPullPars callout = new CalloutParser(parser).parse(); parser.nextTag(); } + if ("no_items_text".equals(parser.getName().toLowerCase())) { + checkNode("no_items_text"); + getNextTagInBlock("no_items_text"); + if ("text".equals(parser.getName().toLowerCase())) { + noItemsText = new TextParser(parser).parse(); + } + continue; + } if ("variables".equals(parser.getName().toLowerCase())) { while (nextTagInBlock("variables")) { String function = parser.getAttributeValue(null, "function"); @@ -112,7 +122,7 @@ public Detail parse() throws InvalidStructureException, IOException, XmlPullPars } } - return new Detail(id, title, nodeset, subdetails, fields, variables, actions, callout, + return new Detail(id, title, noItemsText, nodeset, subdetails, fields, variables, actions, callout, fitAcross, useUniformUnits, forceLandscapeView, focusFunction, printTemplatePath, relevancy, global); } From c4ca7e793c4d8b4b014b9c12163dde0ffdf4d9a3 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Wed, 11 Jan 2023 15:36:33 -0800 Subject: [PATCH 026/402] feat: creates attribute to store noItemsText --- src/main/java/org/commcare/suite/model/Detail.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/org/commcare/suite/model/Detail.java b/src/main/java/org/commcare/suite/model/Detail.java index 1f27092282..a1d0cd7b3c 100755 --- a/src/main/java/org/commcare/suite/model/Detail.java +++ b/src/main/java/org/commcare/suite/model/Detail.java @@ -60,6 +60,7 @@ public class Detail implements Externalizable { private TreeReference nodeset; private DisplayUnit title; + private Text noItemsText; /** * Optional and only relevant if this detail has child details. In that @@ -122,6 +123,7 @@ public Detail(String id, DisplayUnit title, Text noItemsText, String nodeset, Ve this.id = id; this.title = title; + this.noItemsText = noItemsText; if (nodeset != null) { this.nodeset = XPathReference.getPathExpr(nodeset).getReference(); } @@ -179,6 +181,14 @@ public DisplayUnit getTitle() { return title; } + /** + * @return The text to be displayed to users if case list + * has no items. + */ + public Text getNoItemsText() { + return noItemsText; + } + /** * @return A reference to a set of sub-elements of this detail. If provided, * the detail will display fields for each element of this nodeset. @@ -238,6 +248,7 @@ public boolean useAsyncStrategy() { public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { id = (String)ExtUtil.read(in, new ExtWrapNullable(String.class), pf); title = (DisplayUnit)ExtUtil.read(in, DisplayUnit.class, pf); + noItemsText = (Text) ExtUtil.read(in, new ExtWrapNullable(Text.class), pf); titleForm = (String)ExtUtil.read(in, new ExtWrapNullable(String.class), pf); nodeset = (TreeReference)ExtUtil.read(in, new ExtWrapNullable(TreeReference.class), pf); Vector theDetails = (Vector)ExtUtil.read(in, new ExtWrapList(Detail.class), pf); @@ -261,6 +272,7 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOExcep public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.write(out, new ExtWrapNullable(id)); ExtUtil.write(out, title); + ExtUtil.write(out, new ExtWrapNullable(noItemsText)); ExtUtil.write(out, new ExtWrapNullable(titleForm)); ExtUtil.write(out, new ExtWrapNullable(nodeset)); ExtUtil.write(out, new ExtWrapList(ArrayUtilities.toVector(details))); From 019d32552c3b41d3d8ec2a81732380515599d1fc Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Mon, 16 Jan 2023 16:21:53 -0800 Subject: [PATCH 027/402] test: tests no_items_text is parsed from xml and read from appstring --- .../backend/suite/model/test/AppStructureTests.java | 6 ++++++ src/test/resources/app_structure/app_strings.txt | 1 + src/test/resources/app_structure/suite.xml | 10 ++++++++++ 3 files changed, 17 insertions(+) create mode 100644 src/test/resources/app_structure/app_strings.txt diff --git a/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java b/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java index df429d2a7e..d710cac8d3 100644 --- a/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java +++ b/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java @@ -114,6 +114,12 @@ public void testDetailGlobalStructure() { geoOverlay2.getCoordinates().evaluate().getName()); } + @Test + public void testDetailNoItemsText() { + Text noItemsText = mApp.getSession().getPlatform().getDetail("m0_case_short").getNoItemsText(); + Assert.assertEquals("Empty List", noItemsText.evaluate()); + } + @Test public void testDemoUserRestoreParsing() throws Exception { // Test parsing an app with a properly-formed demo user restore file diff --git a/src/test/resources/app_structure/app_strings.txt b/src/test/resources/app_structure/app_strings.txt new file mode 100644 index 0000000000..d52affc4c3 --- /dev/null +++ b/src/test/resources/app_structure/app_strings.txt @@ -0,0 +1 @@ +m0_no_items_text=Empty List \ No newline at end of file diff --git a/src/test/resources/app_structure/suite.xml b/src/test/resources/app_structure/suite.xml index f2187a933d..09f392de1d 100644 --- a/src/test/resources/app_structure/suite.xml +++ b/src/test/resources/app_structure/suite.xml @@ -5,11 +5,21 @@ ./form_placeholder.xml + + + ./app_strings.txt + + <text>Case List</text> + + + + + From 976f862d70babe0bc918300e12898b1010cab6e9 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Wed, 18 Jan 2023 14:10:40 -0800 Subject: [PATCH 028/402] adds Nullable decorator and creates constant variable --- src/main/java/org/commcare/suite/model/Detail.java | 3 +++ src/main/java/org/commcare/xml/DetailParser.java | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/suite/model/Detail.java b/src/main/java/org/commcare/suite/model/Detail.java index a1d0cd7b3c..cb2d5ee1ac 100755 --- a/src/main/java/org/commcare/suite/model/Detail.java +++ b/src/main/java/org/commcare/suite/model/Detail.java @@ -60,6 +60,8 @@ public class Detail implements Externalizable { private TreeReference nodeset; private DisplayUnit title; + + @Nullable private Text noItemsText; /** @@ -185,6 +187,7 @@ public DisplayUnit getTitle() { * @return The text to be displayed to users if case list * has no items. */ + @Nullable public Text getNoItemsText() { return noItemsText; } diff --git a/src/main/java/org/commcare/xml/DetailParser.java b/src/main/java/org/commcare/xml/DetailParser.java index 0612e1829a..9d83e21c9f 100755 --- a/src/main/java/org/commcare/xml/DetailParser.java +++ b/src/main/java/org/commcare/xml/DetailParser.java @@ -21,6 +21,7 @@ * @author ctsims */ public class DetailParser extends CommCareElementParser { + private static final String NAME_NO_ITEMS_TEXT = "no_items_text"; public DetailParser(KXmlParser parser) { super(parser); @@ -73,7 +74,7 @@ public Detail parse() throws InvalidStructureException, IOException, XmlPullPars callout = new CalloutParser(parser).parse(); parser.nextTag(); } - if ("no_items_text".equals(parser.getName().toLowerCase())) { + if (NAME_NO_ITEMS_TEXT.equals(parser.getName().toLowerCase())) { checkNode("no_items_text"); getNextTagInBlock("no_items_text"); if ("text".equals(parser.getName().toLowerCase())) { From e82fd107db37ce9737d9bb157d2b6ea351dd8709 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Tue, 24 Jan 2023 11:08:47 -0800 Subject: [PATCH 029/402] lint --- src/main/java/org/commcare/suite/model/Detail.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/Detail.java b/src/main/java/org/commcare/suite/model/Detail.java index cb2d5ee1ac..3c6bd8c1b5 100755 --- a/src/main/java/org/commcare/suite/model/Detail.java +++ b/src/main/java/org/commcare/suite/model/Detail.java @@ -184,8 +184,9 @@ public DisplayUnit getTitle() { } /** - * @return The text to be displayed to users if case list - * has no items. + * Returns the text displayed when case list results contain no items. + * + * @return returns Text for empty case list. */ @Nullable public Text getNoItemsText() { From 16005dcdae4d467ea5b15fda54d5201a6dfe95ac Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 2 Feb 2023 19:33:59 +0100 Subject: [PATCH 030/402] Change instance root type to AbstractTreeElement to support storage backed tree elements --- .../core/interfaces/RemoteInstanceFetcher.java | 3 ++- .../core/model/instance/ExternalDataInstance.java | 9 ++++++--- .../model/instance/ExternalDataInstanceSource.java | 14 ++++++++------ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/commcare/core/interfaces/RemoteInstanceFetcher.java b/src/main/java/org/commcare/core/interfaces/RemoteInstanceFetcher.java index 7668b7fee6..62b49683cb 100644 --- a/src/main/java/org/commcare/core/interfaces/RemoteInstanceFetcher.java +++ b/src/main/java/org/commcare/core/interfaces/RemoteInstanceFetcher.java @@ -1,5 +1,6 @@ package org.commcare.core.interfaces; +import org.javarosa.core.model.instance.AbstractTreeElement; import org.javarosa.core.model.instance.ExternalDataInstanceSource; import org.javarosa.core.model.instance.TreeElement; @@ -8,7 +9,7 @@ */ public interface RemoteInstanceFetcher { - TreeElement getExternalRoot(String instanceId, ExternalDataInstanceSource source) + AbstractTreeElement getExternalRoot(String instanceId, ExternalDataInstanceSource source) throws RemoteInstanceException; class RemoteInstanceException extends Exception { diff --git a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java index 8ae65e13ce..c0f6e15abd 100644 --- a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java +++ b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java @@ -65,13 +65,16 @@ public ExternalDataInstance(String reference, String instanceId, } public ExternalDataInstance(String reference, String instanceId, - TreeElement topLevel, ExternalDataInstanceSource source) { + AbstractTreeElement topLevel, ExternalDataInstanceSource source) { this(reference, instanceId); base = new InstanceBase(instanceId); this.source = source; - topLevel.setInstanceName(instanceId); - topLevel.setParent(base); this.root = topLevel; + if (root instanceof TreeElement) { + TreeElement rootAsTreeElement = ((TreeElement)root); + rootAsTreeElement.setInstanceName(instanceId); + rootAsTreeElement.setParent(base); + } base.setChild(root); } diff --git a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java index 80b011e754..ed1d646d68 100644 --- a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java +++ b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java @@ -16,7 +16,6 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; -import java.util.UUID; import javax.annotation.Nullable; @@ -27,7 +26,7 @@ public class ExternalDataInstanceSource implements InstanceRoot, Externalizable { @Nullable - private TreeElement root; + private AbstractTreeElement root; private String instanceId; private boolean useCaseTemplate; private String reference; @@ -109,14 +108,14 @@ public boolean needsInit() { return false; } - public TreeElement getRoot() { + public AbstractTreeElement getRoot() { if (needsInit()) { throw new RuntimeException("Uninstantiated external instance source"); } return root; } - public void init(TreeElement root) { + public void init(AbstractTreeElement root) { if (this.root != null) { throw new RuntimeException( "Initializing an already instantiated external instance source is not permitted"); @@ -128,8 +127,11 @@ public void remoteInit(RemoteInstanceFetcher remoteInstanceFetcher) throws RemoteInstanceFetcher.RemoteInstanceException { String instanceId = getInstanceId(); init(remoteInstanceFetcher.getExternalRoot(instanceId, this)); - root.setInstanceName(instanceId); - root.setParent(new InstanceBase(instanceId)); + if (root instanceof TreeElement) { + TreeElement rootAsTreeElement = ((TreeElement)root); + rootAsTreeElement.setInstanceName(instanceId); + rootAsTreeElement.setParent(new InstanceBase(instanceId)); + } } public void setupNewCopy(ExternalDataInstance instance) { From cd4ec6f8ffebc6e8470f5cacdf6aba603dabad12 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 3 Feb 2023 12:38:47 +0100 Subject: [PATCH 031/402] FormLoadingUtils -> InstanceUtils --- .../java/org/commcare/cases/instance/CaseDataInstance.java | 4 ++-- .../utils/{FormLoadingUtils.java => InstanceUtils.java} | 7 +++---- .../core/model/instance/test/DataInstanceTest.java | 4 ++-- src/test/java/org/javarosa/test_utils/ExprEvalUtils.java | 4 ++-- .../org/javarosa/xform/util/test/XmlSerializerTests.java | 4 ++-- 5 files changed, 11 insertions(+), 12 deletions(-) rename src/main/java/org/javarosa/core/model/instance/utils/{FormLoadingUtils.java => InstanceUtils.java} (89%) diff --git a/src/main/java/org/commcare/cases/instance/CaseDataInstance.java b/src/main/java/org/commcare/cases/instance/CaseDataInstance.java index 8dbee8891e..fa662aee44 100644 --- a/src/main/java/org/commcare/cases/instance/CaseDataInstance.java +++ b/src/main/java/org/commcare/cases/instance/CaseDataInstance.java @@ -3,7 +3,7 @@ import org.javarosa.core.model.instance.ExternalDataInstance; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.model.instance.TreeReference; -import org.javarosa.core.model.instance.utils.FormLoadingUtils; +import org.javarosa.core.model.instance.utils.InstanceUtils; import org.javarosa.xml.util.InvalidStructureException; import java.io.IOException; @@ -43,7 +43,7 @@ private static synchronized void loadTemplateSpecLazily() { if (caseDbSpecTemplate == null) { try { caseDbSpecTemplate = - FormLoadingUtils.xmlToTreeElement("/casedb_instance_structure.xml"); + InstanceUtils.xmlToTreeElement("/casedb_instance_structure.xml"); } catch (InvalidStructureException | IOException e) { throw new RuntimeException(errorMsg); } diff --git a/src/main/java/org/javarosa/core/model/instance/utils/FormLoadingUtils.java b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java similarity index 89% rename from src/main/java/org/javarosa/core/model/instance/utils/FormLoadingUtils.java rename to src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java index 26c987f7be..69721d5e62 100644 --- a/src/main/java/org/javarosa/core/model/instance/utils/FormLoadingUtils.java +++ b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java @@ -13,16 +13,15 @@ import java.io.InputStream; /** - * Collection of static form loading methods + * Collection of static instance loading methods * * @author Phillip Mates */ -public class FormLoadingUtils { +public class InstanceUtils { public static FormInstance loadFormInstance(String formFilepath) throws InvalidStructureException, IOException { TreeElement root = xmlToTreeElement(formFilepath); - return new FormInstance(root, null); } @@ -30,7 +29,7 @@ public static TreeElement xmlToTreeElement(String xmlFilepath) throws InvalidStructureException, IOException { InputStream is = null; try { - is = FormLoadingUtils.class.getResourceAsStream(xmlFilepath); + is = InstanceUtils.class.getResourceAsStream(xmlFilepath); TreeElementParser parser = new TreeElementParser(ElementParser.instantiateParser(is), 0, "instance"); try { diff --git a/src/test/java/org/javarosa/core/model/instance/test/DataInstanceTest.java b/src/test/java/org/javarosa/core/model/instance/test/DataInstanceTest.java index d081beb68b..4ab0eceb97 100644 --- a/src/test/java/org/javarosa/core/model/instance/test/DataInstanceTest.java +++ b/src/test/java/org/javarosa/core/model/instance/test/DataInstanceTest.java @@ -3,7 +3,7 @@ import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.FormInstance; import org.javarosa.core.model.instance.TreeReference; -import org.javarosa.core.model.instance.utils.FormLoadingUtils; +import org.javarosa.core.model.instance.utils.InstanceUtils; import org.javarosa.xml.util.InvalidStructureException; import org.javarosa.xpath.XPathException; import org.javarosa.xpath.XPathParseTool; @@ -30,7 +30,7 @@ public void testDataInstance() { // load the xml doc into a form instance FormInstance model = null; try { - model = FormLoadingUtils.loadFormInstance(formPath); + model = InstanceUtils.loadFormInstance(formPath); } catch (IOException e) { fail("Unable to load form at " + formPath); } catch (InvalidStructureException e) { diff --git a/src/test/java/org/javarosa/test_utils/ExprEvalUtils.java b/src/test/java/org/javarosa/test_utils/ExprEvalUtils.java index 7ebdb939b0..1b319a2693 100644 --- a/src/test/java/org/javarosa/test_utils/ExprEvalUtils.java +++ b/src/test/java/org/javarosa/test_utils/ExprEvalUtils.java @@ -2,7 +2,7 @@ import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.FormInstance; -import org.javarosa.core.model.instance.utils.FormLoadingUtils; +import org.javarosa.core.model.instance.utils.InstanceUtils; import org.javarosa.xml.util.InvalidStructureException; import org.javarosa.xpath.XPathException; import org.javarosa.xpath.XPathParseTool; @@ -179,7 +179,7 @@ public static void testEval(String expr, FormInstance model, EvaluationContext e public static FormInstance loadInstance(String formPath) { FormInstance instance = null; try { - instance = FormLoadingUtils.loadFormInstance(formPath); + instance = InstanceUtils.loadFormInstance(formPath); } catch (IOException e) { fail("Unable to load form at " + formPath); } catch (InvalidStructureException e) { diff --git a/src/test/java/org/javarosa/xform/util/test/XmlSerializerTests.java b/src/test/java/org/javarosa/xform/util/test/XmlSerializerTests.java index 011713f44f..267f5229e2 100644 --- a/src/test/java/org/javarosa/xform/util/test/XmlSerializerTests.java +++ b/src/test/java/org/javarosa/xform/util/test/XmlSerializerTests.java @@ -1,7 +1,7 @@ package org.javarosa.xform.util.test; import org.javarosa.core.model.instance.FormInstance; -import org.javarosa.core.model.instance.utils.FormLoadingUtils; +import org.javarosa.core.model.instance.utils.InstanceUtils; import org.javarosa.core.services.transport.payload.ByteArrayPayload; import org.javarosa.core.services.transport.payload.IDataPayload; import org.javarosa.model.xform.XFormSerializingVisitor; @@ -26,7 +26,7 @@ public class XmlSerializerTests { public void testParseXmlWithNonBMPCharacters() { FormInstance model = null; try { - model = FormLoadingUtils.loadFormInstance(formPath); + model = InstanceUtils.loadFormInstance(formPath); // Serialize the xml containing special characters. IDataPayload payload = new XFormSerializingVisitor().createSerializedPayload(model); assertTrue(payload instanceof ByteArrayPayload); From cbc5e2a0179d75f84257ee47c7cd0e5297df1e1b Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 3 Feb 2023 12:44:56 +0100 Subject: [PATCH 032/402] move xmltoTreeElement to TreeUtils --- .../cases/instance/CaseDataInstance.java | 4 +-- .../model/instance/utils/InstanceUtils.java | 25 ++------------- .../model/instance/utils/TreeUtilities.java | 31 +++++++++++++++++++ 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/commcare/cases/instance/CaseDataInstance.java b/src/main/java/org/commcare/cases/instance/CaseDataInstance.java index fa662aee44..9bb2d1d669 100644 --- a/src/main/java/org/commcare/cases/instance/CaseDataInstance.java +++ b/src/main/java/org/commcare/cases/instance/CaseDataInstance.java @@ -3,7 +3,7 @@ import org.javarosa.core.model.instance.ExternalDataInstance; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.model.instance.TreeReference; -import org.javarosa.core.model.instance.utils.InstanceUtils; +import org.javarosa.core.model.instance.utils.TreeUtilities; import org.javarosa.xml.util.InvalidStructureException; import java.io.IOException; @@ -43,7 +43,7 @@ private static synchronized void loadTemplateSpecLazily() { if (caseDbSpecTemplate == null) { try { caseDbSpecTemplate = - InstanceUtils.xmlToTreeElement("/casedb_instance_structure.xml"); + TreeUtilities.xmlToTreeElement("/casedb_instance_structure.xml"); } catch (InvalidStructureException | IOException e) { throw new RuntimeException(errorMsg); } diff --git a/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java index 69721d5e62..b95b8bce57 100644 --- a/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java +++ b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java @@ -1,16 +1,12 @@ package org.javarosa.core.model.instance.utils; -import org.javarosa.core.io.StreamsUtil; +import static org.javarosa.core.model.instance.utils.TreeUtilities.xmlToTreeElement; + import org.javarosa.core.model.instance.FormInstance; import org.javarosa.core.model.instance.TreeElement; -import org.javarosa.xml.ElementParser; -import org.javarosa.xml.TreeElementParser; import org.javarosa.xml.util.InvalidStructureException; -import org.javarosa.xml.util.UnfullfilledRequirementsException; -import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; -import java.io.InputStream; /** * Collection of static instance loading methods @@ -24,21 +20,4 @@ public static FormInstance loadFormInstance(String formFilepath) TreeElement root = xmlToTreeElement(formFilepath); return new FormInstance(root, null); } - - public static TreeElement xmlToTreeElement(String xmlFilepath) - throws InvalidStructureException, IOException { - InputStream is = null; - try { - is = InstanceUtils.class.getResourceAsStream(xmlFilepath); - TreeElementParser parser = new TreeElementParser(ElementParser.instantiateParser(is), 0, "instance"); - - try { - return parser.parse(); - } catch (UnfullfilledRequirementsException | XmlPullParserException e) { - throw new IOException(e.getMessage()); - } - } finally { - StreamsUtil.closeStream(is); - } - } } diff --git a/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java b/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java index 1ac52d8c78..8ed478a7bd 100644 --- a/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java +++ b/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java @@ -1,5 +1,6 @@ package org.javarosa.core.model.instance.utils; +import org.javarosa.core.io.StreamsUtil; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.AbstractTreeElement; import org.javarosa.core.model.instance.FormInstance; @@ -8,6 +9,10 @@ import org.javarosa.core.util.CacheTable; import org.javarosa.core.util.DataUtil; import org.javarosa.model.xform.XPathReference; +import org.javarosa.xml.ElementParser; +import org.javarosa.xml.TreeElementParser; +import org.javarosa.xml.util.InvalidStructureException; +import org.javarosa.xml.util.UnfullfilledRequirementsException; import org.javarosa.xpath.XPathException; import org.javarosa.xpath.expr.FunctionUtils; import org.javarosa.xpath.expr.XPathEqExpr; @@ -15,7 +20,10 @@ import org.javarosa.xpath.expr.XPathPathExpr; import org.javarosa.xpath.expr.XPathStep; import org.javarosa.xpath.expr.XPathStringLiteral; +import org.xmlpull.v1.XmlPullParserException; +import java.io.IOException; +import java.io.InputStream; import java.util.Collection; import java.util.Hashtable; import java.util.LinkedHashSet; @@ -257,4 +265,27 @@ public void visit(AbstractTreeElement element) { }); return copy; } + + /** + * Converts xml in a given file to TreeElement + * @param xmlFilepath file path for the xml file + * @return TreeElement for the given xml + * @throws InvalidStructureException + * @throws IOException + */ + public static TreeElement xmlToTreeElement(String xmlFilepath) + throws InvalidStructureException, IOException { + InputStream is = null; + try { + is = InstanceUtils.class.getResourceAsStream(xmlFilepath); + TreeElementParser parser = new TreeElementParser(ElementParser.instantiateParser(is), 0, "instance"); + try { + return parser.parse(); + } catch (UnfullfilledRequirementsException | XmlPullParserException e) { + throw new IOException(e.getMessage()); + } + } finally { + StreamsUtil.closeStream(is); + } + } } From d94b748b68b0abc5bb3d932bf59169e1ef31a053 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 3 Feb 2023 12:55:24 +0100 Subject: [PATCH 033/402] move parseExternalTree to TreeUtils as xmlStreamToTreeElement --- .../session/RemoteQuerySessionManager.java | 3 ++- .../model/instance/ExternalDataInstance.java | 8 ------- .../model/instance/utils/TreeUtilities.java | 21 +++++++++++++++++++ .../commcare/data/xml/TreeBuilderTest.java | 4 ++-- .../data/xml/VirtualInstancesTest.java | 5 +++-- 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java index d8d4dbddcc..21614af328 100644 --- a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java +++ b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java @@ -17,6 +17,7 @@ import org.javarosa.core.model.instance.ExternalDataInstanceSource; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.model.instance.TreeReference; +import org.javarosa.core.model.instance.utils.TreeUtilities; import org.javarosa.core.model.utils.ItemSetUtils; import org.javarosa.core.util.OrderedHashtable; import org.javarosa.model.xform.XPathReference; @@ -324,7 +325,7 @@ public Pair buildExternalDataInstance(InputStream Multimap requestData) { try { String instanceID = getQueryDatum().getDataId(); - TreeElement root = ExternalDataInstance.parseExternalTree(responseData, instanceID); + TreeElement root = TreeUtilities.xmlStreamToTreeElement(responseData, instanceID); ExternalDataInstanceSource instanceSource = ExternalDataInstanceSource.buildRemote( instanceID, root, getQueryDatum().useCaseTemplate(), url, requestData); ExternalDataInstance instance = instanceSource.toInstance(); diff --git a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java index c0f6e15abd..012ffd457c 100644 --- a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java +++ b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java @@ -78,14 +78,6 @@ public ExternalDataInstance(String reference, String instanceId, base.setChild(root); } - public static TreeElement parseExternalTree(InputStream stream, String instanceId) - throws IOException, UnfullfilledRequirementsException, XmlPullParserException, - InvalidStructureException { - KXmlParser baseParser = ElementParser.instantiateParser(stream); - TreeElement root = new TreeElementParser(baseParser, 0, instanceId).parse(); - return root; - } - public boolean useCaseTemplate() { return source == null ? CaseInstanceTreeElement.MODEL_NAME.equals(instanceid) : source.useCaseTemplate(); } diff --git a/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java b/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java index 8ed478a7bd..5be69d42c8 100644 --- a/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java +++ b/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java @@ -20,6 +20,7 @@ import org.javarosa.xpath.expr.XPathPathExpr; import org.javarosa.xpath.expr.XPathStep; import org.javarosa.xpath.expr.XPathStringLiteral; +import org.kxml2.io.KXmlParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; @@ -268,6 +269,7 @@ public void visit(AbstractTreeElement element) { /** * Converts xml in a given file to TreeElement + * * @param xmlFilepath file path for the xml file * @return TreeElement for the given xml * @throws InvalidStructureException @@ -288,4 +290,23 @@ public static TreeElement xmlToTreeElement(String xmlFilepath) StreamsUtil.closeStream(is); } } + + /** + * Converts a xml stream to TreeElement + * + * @param stream Xml Stream + * @param instanceId Instance Id for the TreeElement + * @return TreeElement for the given xml stream + * @throws IOException + * @throws UnfullfilledRequirementsException + * @throws XmlPullParserException + * @throws InvalidStructureException + */ + public static TreeElement xmlStreamToTreeElement(InputStream stream, String instanceId) + throws IOException, UnfullfilledRequirementsException, XmlPullParserException, + InvalidStructureException { + KXmlParser baseParser = ElementParser.instantiateParser(stream); + TreeElement root = new TreeElementParser(baseParser, 0, instanceId).parse(); + return root; + } } diff --git a/src/test/java/org/commcare/data/xml/TreeBuilderTest.java b/src/test/java/org/commcare/data/xml/TreeBuilderTest.java index 31943900b8..18c9c8313d 100644 --- a/src/test/java/org/commcare/data/xml/TreeBuilderTest.java +++ b/src/test/java/org/commcare/data/xml/TreeBuilderTest.java @@ -3,8 +3,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import org.javarosa.core.model.instance.ExternalDataInstance; import org.javarosa.core.model.instance.TreeElement; +import org.javarosa.core.model.instance.utils.TreeUtilities; import org.javarosa.xml.util.InvalidStructureException; import org.javarosa.xml.util.UnfullfilledRequirementsException; import org.junit.Test; @@ -48,7 +48,7 @@ public void testBuildTree() throws UnfullfilledRequirementsException, XmlPullPar "", "" ); - TreeElement expected = ExternalDataInstance.parseExternalTree( + TreeElement expected = TreeUtilities.xmlStreamToTreeElement( new ByteArrayInputStream(expectedXml.getBytes(StandardCharsets.UTF_8)), "test-instance" ); diff --git a/src/test/java/org/commcare/data/xml/VirtualInstancesTest.java b/src/test/java/org/commcare/data/xml/VirtualInstancesTest.java index 1367c324fc..944fda568a 100644 --- a/src/test/java/org/commcare/data/xml/VirtualInstancesTest.java +++ b/src/test/java/org/commcare/data/xml/VirtualInstancesTest.java @@ -6,6 +6,7 @@ import org.javarosa.core.model.instance.ExternalDataInstance; import org.javarosa.core.model.instance.TreeElement; +import org.javarosa.core.model.instance.utils.TreeUtilities; import org.javarosa.xml.util.InvalidStructureException; import org.javarosa.xml.util.UnfullfilledRequirementsException; import org.junit.Test; @@ -35,7 +36,7 @@ public void testBuildSearchInputRoot() "val2", "" ); - TreeElement expected = ExternalDataInstance.parseExternalTree( + TreeElement expected = TreeUtilities.xmlStreamToTreeElement( new ByteArrayInputStream(expectedXml.getBytes(StandardCharsets.UTF_8)), instanceId ); @@ -56,7 +57,7 @@ public void testBuildSelectedValuesInstance() "case2", "" ); - TreeElement expected = ExternalDataInstance.parseExternalTree( + TreeElement expected = TreeUtilities.xmlStreamToTreeElement( new ByteArrayInputStream(expectedXml.getBytes(StandardCharsets.UTF_8)), instanceId ); From e03158fc772b5fab80f884328af7a9bb39e4397a Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 3 Feb 2023 12:56:22 +0100 Subject: [PATCH 034/402] inline return --- .../org/javarosa/core/model/instance/utils/TreeUtilities.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java b/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java index 5be69d42c8..9047112184 100644 --- a/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java +++ b/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java @@ -306,7 +306,6 @@ public static TreeElement xmlStreamToTreeElement(InputStream stream, String inst throws IOException, UnfullfilledRequirementsException, XmlPullParserException, InvalidStructureException { KXmlParser baseParser = ElementParser.instantiateParser(stream); - TreeElement root = new TreeElementParser(baseParser, 0, instanceId).parse(); - return root; + return new TreeElementParser(baseParser, 0, instanceId).parse(); } } From 44e2f2403facf680dcaeabce613cf87f4f4e0b57 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 3 Feb 2023 13:00:57 +0100 Subject: [PATCH 035/402] adds setUpInstanceRoot --- .../model/instance/ExternalDataInstance.java | 8 +++----- .../instance/ExternalDataInstanceSource.java | 7 ++----- .../model/instance/utils/InstanceUtils.java | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java index 012ffd457c..4d4a0bd3ac 100644 --- a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java +++ b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java @@ -1,5 +1,7 @@ package org.javarosa.core.model.instance; +import static org.javarosa.core.model.instance.utils.InstanceUtils.setUpInstanceRoot; + import org.commcare.cases.instance.CaseInstanceTreeElement; import org.javarosa.core.util.externalizable.DeserializationException; import org.javarosa.core.util.externalizable.ExtUtil; @@ -70,11 +72,7 @@ public ExternalDataInstance(String reference, String instanceId, base = new InstanceBase(instanceId); this.source = source; this.root = topLevel; - if (root instanceof TreeElement) { - TreeElement rootAsTreeElement = ((TreeElement)root); - rootAsTreeElement.setInstanceName(instanceId); - rootAsTreeElement.setParent(base); - } + setUpInstanceRoot(root, instanceId, base); base.setChild(root); } diff --git a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java index ed1d646d68..b57cf5ecde 100644 --- a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java +++ b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java @@ -1,6 +1,7 @@ package org.javarosa.core.model.instance; import static org.javarosa.core.model.instance.ExternalDataInstance.JR_REMOTE_REFERENCE; +import static org.javarosa.core.model.instance.utils.InstanceUtils.setUpInstanceRoot; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; @@ -127,11 +128,7 @@ public void remoteInit(RemoteInstanceFetcher remoteInstanceFetcher) throws RemoteInstanceFetcher.RemoteInstanceException { String instanceId = getInstanceId(); init(remoteInstanceFetcher.getExternalRoot(instanceId, this)); - if (root instanceof TreeElement) { - TreeElement rootAsTreeElement = ((TreeElement)root); - rootAsTreeElement.setInstanceName(instanceId); - rootAsTreeElement.setParent(new InstanceBase(instanceId)); - } + setUpInstanceRoot(root, instanceId, new InstanceBase(instanceId)); } public void setupNewCopy(ExternalDataInstance instance) { diff --git a/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java index b95b8bce57..9dfbb68826 100644 --- a/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java +++ b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java @@ -2,7 +2,9 @@ import static org.javarosa.core.model.instance.utils.TreeUtilities.xmlToTreeElement; +import org.javarosa.core.model.instance.AbstractTreeElement; import org.javarosa.core.model.instance.FormInstance; +import org.javarosa.core.model.instance.InstanceBase; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.xml.util.InvalidStructureException; @@ -20,4 +22,20 @@ public static FormInstance loadFormInstance(String formFilepath) TreeElement root = xmlToTreeElement(formFilepath); return new FormInstance(root, null); } + + /** + * Sets instance properties to the given instance root + * + * @param instanceRoot instance root + * @param instanceId instance id to set + * @param instanceBase instance base to set + */ + public static void setUpInstanceRoot(AbstractTreeElement instanceRoot, String instanceId, + InstanceBase instanceBase) { + if (instanceRoot instanceof TreeElement) { + TreeElement rootAsTreeElement = ((TreeElement)instanceRoot); + rootAsTreeElement.setInstanceName(instanceId); + rootAsTreeElement.setParent(instanceBase); + } + } } From df577e30c81ec39f7d3c8dcbdba36c31ba8b1770 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 3 Feb 2023 13:14:55 +0100 Subject: [PATCH 036/402] reuse xmlStreamToTreeElement --- .../org/javarosa/core/model/instance/utils/TreeUtilities.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java b/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java index 9047112184..7888edd362 100644 --- a/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java +++ b/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java @@ -280,9 +280,8 @@ public static TreeElement xmlToTreeElement(String xmlFilepath) InputStream is = null; try { is = InstanceUtils.class.getResourceAsStream(xmlFilepath); - TreeElementParser parser = new TreeElementParser(ElementParser.instantiateParser(is), 0, "instance"); try { - return parser.parse(); + return xmlStreamToTreeElement(is, "instance"); } catch (UnfullfilledRequirementsException | XmlPullParserException e) { throw new IOException(e.getMessage()); } From 8407847b25cea915fc507862995d330e1b1c04ee Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 7 Feb 2023 18:29:43 +0100 Subject: [PATCH 037/402] throw exception when instance root is not of a expected type --- .../instance/UnrecognisedInstanceRootException.java | 10 ++++++++++ .../core/model/instance/utils/InstanceUtils.java | 5 +++++ 2 files changed, 15 insertions(+) create mode 100644 src/main/java/org/javarosa/core/model/instance/UnrecognisedInstanceRootException.java diff --git a/src/main/java/org/javarosa/core/model/instance/UnrecognisedInstanceRootException.java b/src/main/java/org/javarosa/core/model/instance/UnrecognisedInstanceRootException.java new file mode 100644 index 0000000000..60002f6fed --- /dev/null +++ b/src/main/java/org/javarosa/core/model/instance/UnrecognisedInstanceRootException.java @@ -0,0 +1,10 @@ +package org.javarosa.core.model.instance; + +/** + * Thrown when an instance has an unsupported instance root attached + */ +public class UnrecognisedInstanceRootException extends RuntimeException{ + public UnrecognisedInstanceRootException(String message) { + super(message); + } +} diff --git a/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java index 9dfbb68826..83c4367a82 100644 --- a/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java +++ b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java @@ -6,6 +6,7 @@ import org.javarosa.core.model.instance.FormInstance; import org.javarosa.core.model.instance.InstanceBase; import org.javarosa.core.model.instance.TreeElement; +import org.javarosa.core.model.instance.UnrecognisedInstanceRootException; import org.javarosa.xml.util.InvalidStructureException; import java.io.IOException; @@ -36,6 +37,10 @@ public static void setUpInstanceRoot(AbstractTreeElement instanceRoot, String in TreeElement rootAsTreeElement = ((TreeElement)instanceRoot); rootAsTreeElement.setInstanceName(instanceId); rootAsTreeElement.setParent(instanceBase); + } else { + String error = "Unrecognised Instance root of type " + instanceRoot.getClass().getName() + + " for instance " + instanceId; + throw new UnrecognisedInstanceRootException(error); } } } From 1d8576bf573250af6f4eafc2f41699df67f111e6 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 8 Feb 2023 10:35:14 +0100 Subject: [PATCH 038/402] lint --- .../UnrecognisedInstanceRootException.java | 2 +- .../model/instance/utils/TreeUtilities.java | 30 +++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/instance/UnrecognisedInstanceRootException.java b/src/main/java/org/javarosa/core/model/instance/UnrecognisedInstanceRootException.java index 60002f6fed..095028403c 100644 --- a/src/main/java/org/javarosa/core/model/instance/UnrecognisedInstanceRootException.java +++ b/src/main/java/org/javarosa/core/model/instance/UnrecognisedInstanceRootException.java @@ -3,7 +3,7 @@ /** * Thrown when an instance has an unsupported instance root attached */ -public class UnrecognisedInstanceRootException extends RuntimeException{ +public class UnrecognisedInstanceRootException extends RuntimeException { public UnrecognisedInstanceRootException(String message) { super(message); } diff --git a/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java b/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java index 7888edd362..a293a8ac9e 100644 --- a/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java +++ b/src/main/java/org/javarosa/core/model/instance/utils/TreeUtilities.java @@ -56,11 +56,11 @@ public class TreeUtilities { * is provided */ public static Collection tryBatchChildFetch(AbstractTreeElement parent, - Hashtable> childAttributeHintMap, - String name, - int mult, - Vector predicates, - EvaluationContext evalContext) { + Hashtable> childAttributeHintMap, + String name, + int mult, + Vector predicates, + EvaluationContext evalContext) { // This method builds a predictive model for quick queries that // prevents the need to fully flesh out full walks of the tree. @@ -101,7 +101,8 @@ public static Collection tryBatchChildFetch(AbstractTreeElement p //For now, only cheat when this is a string literal (this basically just means that we're //handling attribute based referencing with very reasonable timing, but it's complex otherwise) - if (left instanceof XPathPathExpr && (right instanceof XPathStringLiteral || right instanceof XPathPathExpr)) { + if (left instanceof XPathPathExpr && (right instanceof XPathStringLiteral + || right instanceof XPathPathExpr)) { String literalMatch = null; if (right instanceof XPathStringLiteral) { literalMatch = ((XPathStringLiteral)right).s; @@ -109,7 +110,8 @@ public static Collection tryBatchChildFetch(AbstractTreeElement p //We'll also try to match direct path queries as long as they are not //complex. - //First: Evaluate whether there are predicates (which may have nesting that ruins our ability to do this) + //First: Evaluate whether there are predicates (which may have nesting that ruins our + // ability to do this) for (XPathStep step : ((XPathPathExpr)right).steps) { if (step.predicates.length > 0) { //We can't evaluate this, just bail @@ -122,7 +124,8 @@ public static Collection tryBatchChildFetch(AbstractTreeElement p Object o = FunctionUtils.unpack(right.eval(evalContext)); literalMatch = FunctionUtils.toString(o); } catch (XPathException e) { - //We may have some weird lack of context that makes this not work, so don't choke on the bonus evaluation + //We may have some weird lack of context that makes this not work, so don't choke on + // the bonus evaluation //and just evaluate that traditional way e.printStackTrace(); break; @@ -141,7 +144,8 @@ public static Collection tryBatchChildFetch(AbstractTreeElement p predicateMatches.add(element.getRef()); } } - //Merge and note that this predicate is evaluated and doesn't need to be evaluated in the future. + //Merge and note that this predicate is evaluated and doesn't need to be evaluated + // in the future. allSelectedChildren = merge(allSelectedChildren, predicateMatches, i, toRemove); continue predicate; } @@ -181,7 +185,7 @@ public static Collection tryBatchChildFetch(AbstractTreeElement p for (int kidI = 0; kidI < kids.size(); ++kidI) { String attrValue = kids.elementAt(kidI).getAttributeValue(null, attributeName); - if(attrValue == null ) { + if (attrValue == null) { attrValue = ""; } @@ -226,8 +230,8 @@ public static Collection tryBatchChildFetch(AbstractTreeElement p private static Collection merge(Collection allSelectedChildren, - Collection predicateMatches, - int i, Vector toRemove) { + Collection predicateMatches, + int i, Vector toRemove) { toRemove.addElement(DataUtil.integer(i)); if (allSelectedChildren == null) { return predicateMatches; @@ -261,7 +265,7 @@ public void visit(FormInstance tree) { @Override public void visit(AbstractTreeElement element) { - ((TreeElement) element).setInstanceName(instanceId); + ((TreeElement)element).setInstanceName(instanceId); } }); return copy; From ea64585841049530f85d1db87f42dcc07e8ed06a Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 9 Feb 2023 22:11:43 +0100 Subject: [PATCH 039/402] adds desciption for different case search prompts --- .../java/org/commcare/suite/model/QueryPrompt.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/org/commcare/suite/model/QueryPrompt.java b/src/main/java/org/commcare/suite/model/QueryPrompt.java index 0aaea1a08e..5653f2559f 100644 --- a/src/main/java/org/commcare/suite/model/QueryPrompt.java +++ b/src/main/java/org/commcare/suite/model/QueryPrompt.java @@ -21,11 +21,22 @@ // Model for node in {@link public class QueryPrompt implements Externalizable { + // Spinner with single selection public static final String INPUT_TYPE_SELECT1 = "select1"; + + // Spinner with multiple selection public static final String INPUT_TYPE_SELECT = "select"; + + // widget to select a date range (start and end date) public static final String INPUT_TYPE_DATERANGE = "daterange"; + + // widget to select a single date public static final String INPUT_TYPE_DATE = "date"; + + // Checkbox, multiple selection public static final String INPUT_TYPE_CHECKBOX = "checkbox"; + + // list of address fields like street, state, city etc public static final String INPUT_TYPE_ADDRESS = "address"; public static final String DEFAULT_REQUIRED_ERROR = "Sorry, this response is required!"; public static final String DEFAULT_VALIDATION_ERROR = "Sorry, this response is invalid!"; From c4b82e9130bd1532076c3bc04e10d5f6a79d24d4 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 9 Feb 2023 22:12:05 +0100 Subject: [PATCH 040/402] method to join multiple choices with delimiter for case search prompts --- .../commcare/session/RemoteQuerySessionManager.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java index d8d4dbddcc..b21552aba6 100644 --- a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java +++ b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java @@ -316,6 +316,16 @@ public static String[] extractMultipleChoices(String answer) { return answer.split(ANSWER_DELIMITER); } + /** + * Join multiple choices for a prompt into a single String separated by answer delimiter + * + * @param choices list of choices to be joined together + * @return String with choices joined with the answer delimiter + */ + public static String joinMultipleChoices(ArrayList choices) { + return String.join(ANSWER_DELIMITER, choices); + } + public RemoteQueryDatum getQueryDatum() { return queryDatum; } From cfb1be8647f366262d9dae8baa752b68fcb3f804 Mon Sep 17 00:00:00 2001 From: Dave Viggiano Date: Mon, 13 Feb 2023 11:19:38 -0500 Subject: [PATCH 041/402] Changes for SAAS-10301. Catching all SSL exceptions in common code rather than catching two specific sub-classes --- .../commcare/core/network/ModernHttpRequester.java | 13 ++++++++++--- .../modern/reference/JavaHttpReference.java | 5 ++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/commcare/core/network/ModernHttpRequester.java b/src/main/java/org/commcare/core/network/ModernHttpRequester.java index 66106cb08c..3429596d00 100644 --- a/src/main/java/org/commcare/core/network/ModernHttpRequester.java +++ b/src/main/java/org/commcare/core/network/ModernHttpRequester.java @@ -18,8 +18,7 @@ import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLException; import okhttp3.FormBody; import okhttp3.MultipartBody; @@ -122,14 +121,22 @@ public Response makeRequest() throws IOException { private Response executeAndCheckCaptivePortals(Call currentCall) throws IOException { try { return currentCall.execute(); - } catch (SSLHandshakeException | SSLPeerUnverifiedException e) { + } catch (SSLException e) { + // SSLHandshakeException is thrown by the CommcareRequestGenerator on + // 4.3 devices when the peer certificate is bad. + // + // SSLPeerUnverifiedException is thrown by the CommcareRequestGenerator + // on 2.3 devices when the peer certificate is bad. + // // This may be a real SSL exception associated with the real endpoint server, or this // might be a property of the local network. + if(NetworkStatus.isCaptivePortal()) { throw new CaptivePortalRedirectException(); } //Otherwise just rethrow the original exception. Probably a certificate issue + //Could be related to local clock issue throw e; } } diff --git a/src/main/java/org/commcare/modern/reference/JavaHttpReference.java b/src/main/java/org/commcare/modern/reference/JavaHttpReference.java index 081c8e10d3..1bae94b460 100644 --- a/src/main/java/org/commcare/modern/reference/JavaHttpReference.java +++ b/src/main/java/org/commcare/modern/reference/JavaHttpReference.java @@ -10,8 +10,7 @@ import java.net.HttpURLConnection; import java.net.URL; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLException; /** * @author ctsims @@ -44,7 +43,7 @@ public InputStream getStream() throws IOException { HttpURLConnection.setFollowRedirects(true); return conn.getInputStream(); - } catch (SSLHandshakeException | SSLPeerUnverifiedException e) { + } catch (SSLException e) { if(NetworkStatus.isCaptivePortal()) { throw new CaptivePortalRedirectException(); } From 5b6ff36a971eb5b0388cb981e9ec68e69cd6402f Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 15 Feb 2023 11:36:50 +0100 Subject: [PATCH 042/402] add instance refId to instance read methods --- .../util/screen/MultiSelectEntityScreen.java | 18 +++++++++++------- .../org/commcare/util/screen/QueryScreen.java | 2 +- .../MemoryVirtualDataInstanceStorage.java | 2 +- .../core/interfaces/RemoteInstanceFetcher.java | 2 +- .../interfaces/VirtualDataInstanceStorage.java | 3 ++- .../commcare/suite/model/StackFrameStep.java | 2 +- .../instance/ExternalDataInstanceSource.java | 4 ++-- .../suite/model/test/QueryModelTests.java | 2 +- 8 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java index 1a6804837e..c28a50e34d 100644 --- a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java @@ -127,8 +127,8 @@ private void setSelectedEntities(String input, @Nullable String[] selectedValues } private void prcessSelectionAsGuid(String guid) throws CommCareSessionException { - ExternalDataInstance cachedInstance = virtualDataInstanceStorage.read( - guid, getSession().getNeededDatum().getDataId()); + String datumId = getNeededDatumId(); + ExternalDataInstance cachedInstance = virtualDataInstanceStorage.read(guid, datumId, datumId); if (cachedInstance == null) { throw new CommCareSessionException( "Could not make selection with reference id " + guid + " on this screen. " + @@ -137,6 +137,10 @@ private void prcessSelectionAsGuid(String guid) throws CommCareSessionException storageReferenceId = guid; } + private String getNeededDatumId() { + return getSession().getNeededDatum().getDataId(); + } + private void processSelectedReferences(TreeReference[] selectedRefs) { if (validateSelectionSize(selectedRefs.length)) { String[] evaluatedValues = new String[selectedRefs.length]; @@ -190,8 +194,8 @@ protected void updateSession(CommCareSession session) { } if (storageReferenceId != null) { if (selectedValuesInstance == null) { - selectedValuesInstance = virtualDataInstanceStorage.read( - storageReferenceId, getSession().getNeededDatum().getDataId()); + String datumId = getNeededDatumId(); + selectedValuesInstance = virtualDataInstanceStorage.read(storageReferenceId, datumId, datumId); } ExternalDataInstanceSource externalDataInstanceSource = ExternalDataInstanceSource.buildVirtual( selectedValuesInstance, storageReferenceId); @@ -203,8 +207,8 @@ protected void updateSession(CommCareSession session) { @Override public void updateDatum(CommCareSession session, String input) { storageReferenceId = input; - String dataId = session.getNeededDatum().getDataId(); - selectedValuesInstance = virtualDataInstanceStorage.read(storageReferenceId, dataId); + String dataId = getNeededDatumId(); + selectedValuesInstance = virtualDataInstanceStorage.read(storageReferenceId, dataId, dataId); ExternalDataInstanceSource externalDataInstanceSource = ExternalDataInstanceSource.buildVirtual( selectedValuesInstance, storageReferenceId); session.setDatum(STATE_MULTIPLE_DATUM_VAL, dataId, input, externalDataInstanceSource); @@ -235,7 +239,7 @@ public String getBreadcrumb(String input, UserSandbox sandbox, SessionWrapper se protected EvaluationContext getAutoLaunchEvaluationContext(String nextInput) { ExternalDataInstance instance; if (referencesContainStep(nextInput)) { - instance = virtualDataInstanceStorage.read(nextInput, "next_input"); + instance = virtualDataInstanceStorage.read(nextInput, "next_input", getNeededDatumId()); } else { // empty instance instance = VirtualInstances.buildSelectedValuesInstance("next_input", new String[0]); diff --git a/src/cli/java/org/commcare/util/screen/QueryScreen.java b/src/cli/java/org/commcare/util/screen/QueryScreen.java index 917d0facda..4578d10626 100644 --- a/src/cli/java/org/commcare/util/screen/QueryScreen.java +++ b/src/cli/java/org/commcare/util/screen/QueryScreen.java @@ -184,7 +184,7 @@ private ExternalDataInstance getUserInputInstance() { Map userQueryValues = remoteQuerySessionManager.getUserQueryValues(false); String key = getInstanceKey(instanceId, userQueryValues); if (instanceStorage.contains(key)) { - return instanceStorage.read(key, instanceId); + return instanceStorage.read(key, instanceId, refId); } ExternalDataInstance userInputInstance = VirtualInstances.buildSearchInputInstance( diff --git a/src/main/java/org/commcare/core/interfaces/MemoryVirtualDataInstanceStorage.java b/src/main/java/org/commcare/core/interfaces/MemoryVirtualDataInstanceStorage.java index c0e10ee356..9b432e57b3 100644 --- a/src/main/java/org/commcare/core/interfaces/MemoryVirtualDataInstanceStorage.java +++ b/src/main/java/org/commcare/core/interfaces/MemoryVirtualDataInstanceStorage.java @@ -31,7 +31,7 @@ public String write(String key, ExternalDataInstance dataInstance) { } @Override - public ExternalDataInstance read(String key, String instanceId) { + public ExternalDataInstance read(String key, String instanceId, String refId) { return storage.get(key); } diff --git a/src/main/java/org/commcare/core/interfaces/RemoteInstanceFetcher.java b/src/main/java/org/commcare/core/interfaces/RemoteInstanceFetcher.java index 62b49683cb..ef62900a60 100644 --- a/src/main/java/org/commcare/core/interfaces/RemoteInstanceFetcher.java +++ b/src/main/java/org/commcare/core/interfaces/RemoteInstanceFetcher.java @@ -9,7 +9,7 @@ */ public interface RemoteInstanceFetcher { - AbstractTreeElement getExternalRoot(String instanceId, ExternalDataInstanceSource source) + AbstractTreeElement getExternalRoot(String instanceId, ExternalDataInstanceSource source, String refId) throws RemoteInstanceException; class RemoteInstanceException extends Exception { diff --git a/src/main/java/org/commcare/core/interfaces/VirtualDataInstanceStorage.java b/src/main/java/org/commcare/core/interfaces/VirtualDataInstanceStorage.java index 6ddee5d0bd..de4e3c9302 100644 --- a/src/main/java/org/commcare/core/interfaces/VirtualDataInstanceStorage.java +++ b/src/main/java/org/commcare/core/interfaces/VirtualDataInstanceStorage.java @@ -15,8 +15,9 @@ public interface VirtualDataInstanceStorage { * * @param key The instance storage key * @param instanceId The instanceId to apply to the loaded instance + * @param refId Unique reference id to apply to the loaded instance */ - ExternalDataInstance read(String key, String instanceId); + ExternalDataInstance read(String key, String instanceId, String refId); boolean contains(String key); } diff --git a/src/main/java/org/commcare/suite/model/StackFrameStep.java b/src/main/java/org/commcare/suite/model/StackFrameStep.java index 8209a011dc..96573031bb 100644 --- a/src/main/java/org/commcare/suite/model/StackFrameStep.java +++ b/src/main/java/org/commcare/suite/model/StackFrameStep.java @@ -139,7 +139,7 @@ public void initDataInstanceSources(RemoteInstanceFetcher remoteInstanceFetcher) throws RemoteInstanceFetcher.RemoteInstanceException { for (ExternalDataInstanceSource source : dataInstanceSources.values()) { if (source.needsInit()) { - source.remoteInit(remoteInstanceFetcher); + source.remoteInit(remoteInstanceFetcher, getId()); } } } diff --git a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java index b57cf5ecde..d11e07a611 100644 --- a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java +++ b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java @@ -124,10 +124,10 @@ public void init(AbstractTreeElement root) { this.root = root; } - public void remoteInit(RemoteInstanceFetcher remoteInstanceFetcher) + public void remoteInit(RemoteInstanceFetcher remoteInstanceFetcher, String refID) throws RemoteInstanceFetcher.RemoteInstanceException { String instanceId = getInstanceId(); - init(remoteInstanceFetcher.getExternalRoot(instanceId, this)); + init(remoteInstanceFetcher.getExternalRoot(instanceId, this, refID)); setUpInstanceRoot(root, instanceId, new InstanceBase(instanceId)); } diff --git a/src/test/java/org/commcare/backend/suite/model/test/QueryModelTests.java b/src/test/java/org/commcare/backend/suite/model/test/QueryModelTests.java index 425c04c70d..e93e29c40e 100644 --- a/src/test/java/org/commcare/backend/suite/model/test/QueryModelTests.java +++ b/src/test/java/org/commcare/backend/suite/model/test/QueryModelTests.java @@ -69,7 +69,7 @@ public void testScreenCreatesVirtualInstance() throws Exception { Map input = ImmutableMap.of("name", "bob", "age", "23"); Assert.assertEquals( VirtualInstances.buildSearchInputInstance(refId, input).getRoot(), - virtualDataInstanceStorage.read(expectedInstanceStorageKey, instanceID).getRoot()); + virtualDataInstanceStorage.read(expectedInstanceStorageKey, instanceID, refId).getRoot()); CaseTestUtils.xpathEvalAndAssert( session.getEvaluationContext(), From 506c9e2166f056b95aa30d6a4c22414761f38834 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 15 Feb 2023 15:51:23 +0100 Subject: [PATCH 043/402] lint:refID -> refId --- .../core/model/instance/ExternalDataInstanceSource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java index d11e07a611..78ef63f70c 100644 --- a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java +++ b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstanceSource.java @@ -124,10 +124,10 @@ public void init(AbstractTreeElement root) { this.root = root; } - public void remoteInit(RemoteInstanceFetcher remoteInstanceFetcher, String refID) + public void remoteInit(RemoteInstanceFetcher remoteInstanceFetcher, String refId) throws RemoteInstanceFetcher.RemoteInstanceException { String instanceId = getInstanceId(); - init(remoteInstanceFetcher.getExternalRoot(instanceId, this, refID)); + init(remoteInstanceFetcher.getExternalRoot(instanceId, this, refId)); setUpInstanceRoot(root, instanceId, new InstanceBase(instanceId)); } From 815731e7ae3641dc142dc0bdb2fffc53b6b914e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 02:27:44 +0000 Subject: [PATCH 044/402] Bump me.champeau.jmh from 0.6.5 to 0.7.0 Bumps me.champeau.jmh from 0.6.5 to 0.7.0. --- updated-dependencies: - dependency-name: me.champeau.jmh dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d72b460716..b60d2d8a31 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { plugins { id 'java-library' - id "me.champeau.jmh" version "0.6.5" + id "me.champeau.jmh" version "0.7.0" } // Needed even though it looks unused From f5f5d74c7e2f8c7c004d2c77226ea06f6bbe81d4 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 1 Mar 2023 02:40:16 +0530 Subject: [PATCH 045/402] reuse value --- .../java/org/commcare/session/RemoteQuerySessionManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java index b21552aba6..ce95565b59 100644 --- a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java +++ b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java @@ -153,7 +153,7 @@ public Multimap getRawQueryParams(boolean skipDefaultPromptValue XPathExpression excludeExpr = prompt.getExclude(); if (!(params.containsKey(key) && params.get(key).contains(value))) { if (value != null && (excludeExpr == null || !(boolean)excludeExpr.eval(evaluationContext))) { - params.put(key, userAnswers.get(key)); + params.put(key, value); } } } From f10e9f1506dbb8a1580fd47f4936362efed786c3 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 1 Mar 2023 02:48:27 +0530 Subject: [PATCH 046/402] separate out multi valued answers while evaluating query params for query request --- .../java/org/commcare/session/RemoteQuerySessionManager.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java index ce95565b59..c9b2121595 100644 --- a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java +++ b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java @@ -153,7 +153,10 @@ public Multimap getRawQueryParams(boolean skipDefaultPromptValue XPathExpression excludeExpr = prompt.getExclude(); if (!(params.containsKey(key) && params.get(key).contains(value))) { if (value != null && (excludeExpr == null || !(boolean)excludeExpr.eval(evaluationContext))) { - params.put(key, value); + String[] choices = RemoteQuerySessionManager.extractMultipleChoices(value); + for (String choice : choices) { + params.put(key, choice); + } } } } From a014f35da2767e3a922c8e861f3816e3867b154a Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 1 Mar 2023 03:39:27 +0530 Subject: [PATCH 047/402] remove reundant multi-valued answers handling --- src/cli/java/org/commcare/util/screen/QueryScreen.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/QueryScreen.java b/src/cli/java/org/commcare/util/screen/QueryScreen.java index 917d0facda..8ff38724e8 100644 --- a/src/cli/java/org/commcare/util/screen/QueryScreen.java +++ b/src/cli/java/org/commcare/util/screen/QueryScreen.java @@ -142,16 +142,8 @@ public Multimap getRequestData(boolean skipDefaultPromptValues) ImmutableListMultimap.Builder dataBuilder = ImmutableListMultimap.builder(); Multimap queryParams = getQueryParams(skipDefaultPromptValues); for (String key : queryParams.keySet()) { - QueryPrompt prompt = userInputDisplays.get(key); for (String value : queryParams.get(key)) { - if (prompt != null) { - String[] choices = RemoteQuerySessionManager.extractMultipleChoices(value); - for (String choice : choices) { - dataBuilder.put(key, choice); - } - } else { - dataBuilder.put(key, value); - } + dataBuilder.put(key, value); } } return dataBuilder.build(); From 7bdb52f5abe9e8abb99aa47cd178b39e07ce3d49 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 1 Mar 2023 03:44:32 +0530 Subject: [PATCH 048/402] request data is redundant now as it just copies the multimap, remove it --- .../java/org/commcare/util/screen/QueryScreen.java | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/QueryScreen.java b/src/cli/java/org/commcare/util/screen/QueryScreen.java index 8ff38724e8..29d35b640e 100644 --- a/src/cli/java/org/commcare/util/screen/QueryScreen.java +++ b/src/cli/java/org/commcare/util/screen/QueryScreen.java @@ -137,18 +137,6 @@ private ArrayList getSupportedPrompts() { supportedPrompts.add(INPUT_TYPE_ADDRESS); return supportedPrompts; } - - public Multimap getRequestData(boolean skipDefaultPromptValues) { - ImmutableListMultimap.Builder dataBuilder = ImmutableListMultimap.builder(); - Multimap queryParams = getQueryParams(skipDefaultPromptValues); - for (String key : queryParams.keySet()) { - for (String value : queryParams.get(key)) { - dataBuilder.put(key, value); - } - } - return dataBuilder.build(); - } - public Pair processResponse(InputStream responseData, URL url, Multimap requestData) { if (responseData == null) { @@ -279,7 +267,7 @@ public boolean handleInputAndUpdateSession(CommCareSession session, String input } answerPrompts(userAnswers); URL url = getBaseUrl(); - Multimap requestData = getRequestData(false); + Multimap requestData = getQueryParams(false); InputStream response = sessionUtils.makeQueryRequest(url, requestData, domainedUsername, password); Pair instanceOrError = processResponse(response, url, requestData); updateSession(instanceOrError.first); From 00c333c7ebc8f394e97c1b19d2be7318bba366b7 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 1 Mar 2023 03:53:17 +0530 Subject: [PATCH 049/402] make getQueryParams public as it replaces getRequestData now --- src/cli/java/org/commcare/util/screen/QueryScreen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/java/org/commcare/util/screen/QueryScreen.java b/src/cli/java/org/commcare/util/screen/QueryScreen.java index 29d35b640e..1a6390c7d2 100644 --- a/src/cli/java/org/commcare/util/screen/QueryScreen.java +++ b/src/cli/java/org/commcare/util/screen/QueryScreen.java @@ -225,7 +225,7 @@ public URL getBaseUrl() { * @param skipDefaultPromptValues don't apply the default value expressions for query prompts * @return filters to be applied to case search uri as query params */ - protected Multimap getQueryParams(boolean skipDefaultPromptValues) { + public Multimap getQueryParams(boolean skipDefaultPromptValues) { return remoteQuerySessionManager.getRawQueryParams(skipDefaultPromptValues); } From 9e85934df63fa8ca51fa794f5292ddb7d2f5f7e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Mar 2023 01:21:09 +0000 Subject: [PATCH 050/402] Bump com.datadoghq:dd-trace-api from 0.64.0 to 1.10.0 Bumps [com.datadoghq:dd-trace-api](https://github.com/datadog/dd-trace-java) from 0.64.0 to 1.10.0. - [Release notes](https://github.com/datadog/dd-trace-java/releases) - [Commits](https://github.com/datadog/dd-trace-java/compare/v0.64.0...v1.10.0) --- updated-dependencies: - dependency-name: com.datadoghq:dd-trace-api dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 6652cffac0..736125c60e 100644 --- a/build.gradle +++ b/build.gradle @@ -82,7 +82,7 @@ dependencies { implementation 'com.google.guava:guava:30.1.1-jre' implementation 'io.opentracing:opentracing-api:0.33.0' implementation 'io.opentracing:opentracing-util:0.33.0' - implementation 'com.datadoghq:dd-trace-api:0.64.0' + implementation 'com.datadoghq:dd-trace-api:1.10.0' cliImplementation 'org.json:json:20140107' cliImplementation 'com.github.stefanhaustein:kxml2:2.4.1' @@ -92,7 +92,7 @@ dependencies { cliImplementation 'com.squareup.retrofit2:retrofit:2.3.0' cliImplementation 'com.squareup.okhttp3:okhttp:4.4.1' cliImplementation 'com.google.guava:guava:30.1.1-jre' - cliImplementation 'com.datadoghq:dd-trace-api:0.64.0' + cliImplementation 'com.datadoghq:dd-trace-api:1.10.0' testImplementation sourceSets.main.output testImplementation sourceSets.ccapi.output @@ -118,7 +118,7 @@ dependencies { ccapiImplementation 'com.google.guava:guava:30.1.1-jre' ccapiImplementation 'io.opentracing:opentracing-api:0.33.0' ccapiImplementation 'io.opentracing:opentracing-util:0.33.0' - ccapiImplementation 'com.datadoghq:dd-trace-api:0.64.0' + ccapiImplementation 'com.datadoghq:dd-trace-api:1.10.0' translateImplementation 'com.github.stefanhaustein:kxml2:2.4.1' translateImplementation 'xpp3:xpp3:1.1.4c' From 6eb5a5e830687d2fa9d433c52a8cea6636f58ef8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Mar 2023 07:40:30 +0000 Subject: [PATCH 051/402] Bump guava from 30.1.1-jre to 31.1-jre Bumps [guava](https://github.com/google/guava) from 30.1.1-jre to 31.1-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 736125c60e..e7629f7eb6 100644 --- a/build.gradle +++ b/build.gradle @@ -79,7 +79,7 @@ dependencies { api 'com.squareup.okhttp3:okhttp:3.12.12' implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'io.reactivex.rxjava2:rxjava:2.2.21' - implementation 'com.google.guava:guava:30.1.1-jre' + implementation 'com.google.guava:guava:31.1-jre' implementation 'io.opentracing:opentracing-api:0.33.0' implementation 'io.opentracing:opentracing-util:0.33.0' implementation 'com.datadoghq:dd-trace-api:1.10.0' @@ -91,7 +91,7 @@ dependencies { cliImplementation 'io.reactivex.rxjava2:rxjava:2.1.1' cliImplementation 'com.squareup.retrofit2:retrofit:2.3.0' cliImplementation 'com.squareup.okhttp3:okhttp:4.4.1' - cliImplementation 'com.google.guava:guava:30.1.1-jre' + cliImplementation 'com.google.guava:guava:31.1-jre' cliImplementation 'com.datadoghq:dd-trace-api:1.10.0' testImplementation sourceSets.main.output @@ -104,7 +104,7 @@ dependencies { testImplementation 'joda-time:joda-time:2.9.4' testImplementation 'com.squareup.retrofit2:retrofit:2.3.0' testImplementation 'com.squareup.okhttp3:okhttp:4.4.1' - testImplementation 'com.google.guava:guava:30.1.1-jre' + testImplementation 'com.google.guava:guava:31.1-jre' ccapiImplementation sourceSets.main.output ccapiImplementation 'org.json:json:20140107' @@ -115,7 +115,7 @@ dependencies { ccapiImplementation 'io.reactivex.rxjava2:rxjava:2.1.1' ccapiImplementation "com.squareup.retrofit2:retrofit:2.3.0" ccapiImplementation 'com.squareup.okhttp3:okhttp:4.4.1' - ccapiImplementation 'com.google.guava:guava:30.1.1-jre' + ccapiImplementation 'com.google.guava:guava:31.1-jre' ccapiImplementation 'io.opentracing:opentracing-api:0.33.0' ccapiImplementation 'io.opentracing:opentracing-util:0.33.0' ccapiImplementation 'com.datadoghq:dd-trace-api:1.10.0' @@ -129,7 +129,7 @@ dependencies { translateImplementation 'io.reactivex.rxjava2:rxjava:2.1.1' translateImplementation "com.squareup.retrofit2:retrofit:2.3.0" translateImplementation 'com.squareup.okhttp3:okhttp:4.4.1' - translateImplementation 'com.google.guava:guava:30.1.1-jre' + translateImplementation 'com.google.guava:guava:31.1-jre' // 'compile' dependency is also needed to run 'gradle jmh' for some reason, // but leaving it in conflicts with apk builds, so only uncomment the From af354b4bb147a2aa4ca9deff59e469b1a96ef3ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 02:00:45 +0000 Subject: [PATCH 052/402] Bump com.carrotsearch:hppc from 0.7.2 to 0.9.1 Bumps [com.carrotsearch:hppc](https://github.com/carrotsearch/hppc) from 0.7.2 to 0.9.1. - [Release notes](https://github.com/carrotsearch/hppc/releases) - [Changelog](https://github.com/carrotsearch/hppc/blob/master/CHANGES.txt) - [Commits](https://github.com/carrotsearch/hppc/compare/0.7.2...0.9.1) --- updated-dependencies: - dependency-name: com.carrotsearch:hppc dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e7629f7eb6..5b1b9d3f4c 100644 --- a/build.gradle +++ b/build.gradle @@ -111,7 +111,7 @@ dependencies { ccapiImplementation 'org.xerial:sqlite-jdbc:3.40.0.0' ccapiImplementation 'com.github.stefanhaustein:kxml2:2.4.1' ccapiImplementation 'joda-time:joda-time:2.9.4' - ccapiImplementation 'com.carrotsearch:hppc:0.7.2' + ccapiImplementation 'com.carrotsearch:hppc:0.9.1' ccapiImplementation 'io.reactivex.rxjava2:rxjava:2.1.1' ccapiImplementation "com.squareup.retrofit2:retrofit:2.3.0" ccapiImplementation 'com.squareup.okhttp3:okhttp:4.4.1' From 87133c84d763dc9b4dd473b15c24826c106e1cf0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Mar 2023 01:13:18 +0000 Subject: [PATCH 053/402] Bump javax.ws.rs:javax.ws.rs-api from 2.0.1 to 2.1.1 Bumps [javax.ws.rs:javax.ws.rs-api](https://github.com/eclipse-ee4j/jaxrs-api) from 2.0.1 to 2.1.1. - [Release notes](https://github.com/eclipse-ee4j/jaxrs-api/releases) - [Changelog](https://github.com/jakartaee/rest/blob/master/jaxrs-tck-docs/JAXRSTCK3.1-ReleaseNotes.html) - [Commits](https://github.com/eclipse-ee4j/jaxrs-api/commits/2.1.1) --- updated-dependencies: - dependency-name: javax.ws.rs:javax.ws.rs-api dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5b1b9d3f4c..830f2f2b96 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ sourceSets { dependencies { api 'com.github.stefanhaustein:kxml2:2.4.1' - implementation 'javax.ws.rs:javax.ws.rs-api:2.0.1' + implementation 'javax.ws.rs:javax.ws.rs-api:2.1.1' // Added so Android Studio recognizes libs in util jar projects implementation 'org.json:json:20220924' implementation 'commons-cli:commons-cli:1.3.1' From 08bd95e17db73286c5f8d0909bdfb18cd7f61423 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 9 Mar 2023 14:53:27 +0530 Subject: [PATCH 054/402] Revert "Bump javax.ws.rs:javax.ws.rs-api from 2.0.1 to 2.1.1" --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 830f2f2b96..5b1b9d3f4c 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ sourceSets { dependencies { api 'com.github.stefanhaustein:kxml2:2.4.1' - implementation 'javax.ws.rs:javax.ws.rs-api:2.1.1' + implementation 'javax.ws.rs:javax.ws.rs-api:2.0.1' // Added so Android Studio recognizes libs in util jar projects implementation 'org.json:json:20220924' implementation 'commons-cli:commons-cli:1.3.1' From 4422c6dac92c9148e3a0f5a6c14a45fd012702ff Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Tue, 28 Mar 2023 15:42:56 -0700 Subject: [PATCH 055/402] feat: parses input --- scripts/duplicate_pr.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 scripts/duplicate_pr.py diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py new file mode 100644 index 0000000000..820e5a10fd --- /dev/null +++ b/scripts/duplicate_pr.py @@ -0,0 +1,19 @@ +from enum import Enum + +import argparse + +class BranchName(Enum): + MASTER = "master" + FORMPLAYER = "formplayer" + + +def main(): + parser = argparse.ArgumentParser(description='Duplicate and push PR between formplayer/master') + parser.add_argument('orig_pr_id', type=str, help="ID of PR to be cloned") + parser.add_argument('orig_source_branch', type=str, help="Branch name of PR to be duplicated") + parser.add_argument('orig_target_branch', type=str, help="Name of branch the original PR merged into", + choices = [key.value for key in BranchName]) + args = parser.parse_args() + +if __name__ == "__main__": + main() \ No newline at end of file From a7081de6399c77b8791f439bf66de1380620d391 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Tue, 28 Mar 2023 15:55:56 -0700 Subject: [PATCH 056/402] feat: bake argument for git --- scripts/duplicate_pr.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 820e5a10fd..c7b999b593 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -1,12 +1,18 @@ from enum import Enum import argparse +import sh + class BranchName(Enum): MASTER = "master" FORMPLAYER = "formplayer" +def get_git(path=None): + return sh.git.bake(_tty_out=False, _cwd=path) + + def main(): parser = argparse.ArgumentParser(description='Duplicate and push PR between formplayer/master') parser.add_argument('orig_pr_id', type=str, help="ID of PR to be cloned") From 19b0f7a4ea2274ef6d9433624b998bd777babb8a Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Tue, 28 Mar 2023 15:58:40 -0700 Subject: [PATCH 057/402] feat: defines new source and target branch --- scripts/duplicate_pr.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index c7b999b593..e9a3d98f5e 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -13,6 +13,16 @@ def get_git(path=None): return sh.git.bake(_tty_out=False, _cwd=path) +def get_target_branch(orig_target_branch:str): + if orig_target_branch == BranchName.FORMPLAYER.value: + return BranchName.MASTER.value + elif orig_target_branch == BranchName.MASTER.value: + return BranchName.FORMPLAYER.value + else: + print("Only following branches are allowed: {}".format(", ".join([key.value for key in BranchName]))) + exit(1) + + def main(): parser = argparse.ArgumentParser(description='Duplicate and push PR between formplayer/master') parser.add_argument('orig_pr_id', type=str, help="ID of PR to be cloned") @@ -21,5 +31,9 @@ def main(): choices = [key.value for key in BranchName]) args = parser.parse_args() + new_source_branch = "copy_of_" + args.orig_source_branch + new_target_branch = get_target_branch(args.orig_target_branch) + + if __name__ == "__main__": main() \ No newline at end of file From 4788b7bbef6f5bf505ff6f1fb990e5d07e15a910 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Tue, 28 Mar 2023 16:03:03 -0700 Subject: [PATCH 058/402] feat: creates copy branch --- scripts/duplicate_pr.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index e9a3d98f5e..1d86ca26a3 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -23,6 +23,20 @@ def get_target_branch(orig_target_branch:str): exit(1) +def git_create_branch(orig_branch_name:str, new_branch_name: str, git=None): + git = git or get_git() + try: + git.checkout(orig_branch_name) + except sh.ErrorReturnCode_1 as e: + print(e.stderr.decode()) + exit(1) + try: + git.checkout('-b', new_branch_name) + except sh.ErrorReturnCode_128 as e: + print(e.stderr.decode()) + exit(1) + + def main(): parser = argparse.ArgumentParser(description='Duplicate and push PR between formplayer/master') parser.add_argument('orig_pr_id', type=str, help="ID of PR to be cloned") @@ -34,6 +48,9 @@ def main(): new_source_branch = "copy_of_" + args.orig_source_branch new_target_branch = get_target_branch(args.orig_target_branch) + print("Creating branch {} from {}".format(new_source_branch, new_target_branch)) + git_create_branch(orig_branch_name=new_target_branch, new_branch_name=new_source_branch) + if __name__ == "__main__": main() \ No newline at end of file From 8cc7f804922a2bc7a3500a0227353fd1b008b9dc Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Tue, 28 Mar 2023 16:04:09 -0700 Subject: [PATCH 059/402] feat: red text for errors --- scripts/duplicate_pr.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 1d86ca26a3..32c107beea 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -28,15 +28,29 @@ def git_create_branch(orig_branch_name:str, new_branch_name: str, git=None): try: git.checkout(orig_branch_name) except sh.ErrorReturnCode_1 as e: - print(e.stderr.decode()) + print(red(e.stderr.decode())) exit(1) try: git.checkout('-b', new_branch_name) except sh.ErrorReturnCode_128 as e: - print(e.stderr.decode()) + print(red(e.stderr.decode())) exit(1) +def _wrap_with(code): + + def inner(text, bold=False): + c = code + + if bold: + c = "1;%s" % c + return "\033[%sm%s\033[0m" % (c, text) + return inner + + +red = _wrap_with('31') + + def main(): parser = argparse.ArgumentParser(description='Duplicate and push PR between formplayer/master') parser.add_argument('orig_pr_id', type=str, help="ID of PR to be cloned") From 2422a09e557d6eab4e62eddd139c2a907fa0c5a8 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Tue, 28 Mar 2023 16:05:19 -0700 Subject: [PATCH 060/402] feat: pulls pr to be duplicated --- scripts/duplicate_pr.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 32c107beea..eeb9bab469 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -37,6 +37,16 @@ def git_create_branch(orig_branch_name:str, new_branch_name: str, git=None): exit(1) +def git_pull_pr(pr_id:str, new_branch_name:str, git=None): + git = git or get_git() + input = "pull/{}/head:{}".format(pr_id,new_branch_name) + try: + git.fetch("origin", input) + except sh.ErrorReturnCode_1 as e: + print(red(e.stderr.decode())) + exit(1) + + def _wrap_with(code): def inner(text, bold=False): @@ -65,6 +75,9 @@ def main(): print("Creating branch {} from {}".format(new_source_branch, new_target_branch)) git_create_branch(orig_branch_name=new_target_branch, new_branch_name=new_source_branch) + print("Pulling {}".format(args.orig_source_branch)) + git_pull_pr(args.orig_pr_id, args.orig_source_branch) + if __name__ == "__main__": main() \ No newline at end of file From 655c922ded3e562e7a708633821f8a8dd933eb71 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Tue, 28 Mar 2023 16:09:26 -0700 Subject: [PATCH 061/402] feat: get new commits between two branches --- scripts/duplicate_pr.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index eeb9bab469..0a3a6c076c 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -47,6 +47,26 @@ def git_pull_pr(pr_id:str, new_branch_name:str, git=None): exit(1) +def get_new_commits(base_branch: str, curr_branch:str, git=None): + git = git or get_git() + base_commit = merge_base_commit(base_branch, curr_branch) + recent_commit = latest_commit(curr_branch) + + commits_range = "{}..{}".format(base_commit, recent_commit) + interested_commits = git("rev-list", "--no-merges", commits_range).split() + return interested_commits + + +def merge_base_commit(branch1: str, branch2:str, git=None): + git = git or get_git() + return str(git("merge-base", branch1, branch2).replace("\n", "")) + + +def latest_commit(branch:str, git=None): + git = git or get_git() + return str(git("rev-parse", branch).replace("\n", "")) + + def _wrap_with(code): def inner(text, bold=False): @@ -78,6 +98,9 @@ def main(): print("Pulling {}".format(args.orig_source_branch)) git_pull_pr(args.orig_pr_id, args.orig_source_branch) + print("Getting new commits from {}".format(args.orig_source_branch)) + new_commits = get_new_commits(args.orig_target_branch, args.orig_source_branch) + if __name__ == "__main__": main() \ No newline at end of file From 184bc73bcb04ba06c81644272171edaa1fbbed0a Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Tue, 28 Mar 2023 16:10:33 -0700 Subject: [PATCH 062/402] feat: cherry-pick commits onto specified branch --- scripts/duplicate_pr.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 0a3a6c076c..f24ad321f1 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -57,6 +57,13 @@ def get_new_commits(base_branch: str, curr_branch:str, git=None): return interested_commits +def cherry_pick_new_commits(commits:list[str], branch:str, git=None): + git = git or get_git() + git.checkout(branch) + for commits in reversed(commits): + git("cherry-pick", commits) + + def merge_base_commit(branch1: str, branch2:str, git=None): git = git or get_git() return str(git("merge-base", branch1, branch2).replace("\n", "")) @@ -101,6 +108,9 @@ def main(): print("Getting new commits from {}".format(args.orig_source_branch)) new_commits = get_new_commits(args.orig_target_branch, args.orig_source_branch) + print("Cherry-picking commits from {} to {}".format(args.orig_source_branch, new_source_branch)) + cherry_pick_new_commits (new_commits, new_source_branch) + if __name__ == "__main__": main() \ No newline at end of file From 53c1d3e3cc03438664821f9b4be89c385218ab00 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Tue, 28 Mar 2023 16:13:38 -0700 Subject: [PATCH 063/402] feat: pushes new branch --- scripts/duplicate_pr.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index f24ad321f1..13b1b8754f 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -64,6 +64,11 @@ def cherry_pick_new_commits(commits:list[str], branch:str, git=None): git("cherry-pick", commits) +def git_push_pr(branch:str, git=None): + git = git or get_git() + git.push("origin", branch) + + def merge_base_commit(branch1: str, branch2:str, git=None): git = git or get_git() return str(git("merge-base", branch1, branch2).replace("\n", "")) @@ -111,6 +116,9 @@ def main(): print("Cherry-picking commits from {} to {}".format(args.orig_source_branch, new_source_branch)) cherry_pick_new_commits (new_commits, new_source_branch) + print("Pushing {}".format(new_source_branch)) + git_push_pr(new_source_branch) + if __name__ == "__main__": - main() \ No newline at end of file + main() From 7dc85276da51883359065ef43468a3e4fd419df0 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Wed, 29 Mar 2023 13:34:23 -0700 Subject: [PATCH 064/402] refactor: PR ID not needed to pull branch --- scripts/duplicate_pr.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 13b1b8754f..77c30214b4 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -37,9 +37,10 @@ def git_create_branch(orig_branch_name:str, new_branch_name: str, git=None): exit(1) -def git_pull_pr(pr_id:str, new_branch_name:str, git=None): +def git_fetch_pr_branch(branch_name:str, git=None): git = git or get_git() - input = "pull/{}/head:{}".format(pr_id,new_branch_name) + # fetch remote branch without switching branches + input = "{0}:{0}".format(branch_name) try: git.fetch("origin", input) except sh.ErrorReturnCode_1 as e: @@ -95,7 +96,6 @@ def inner(text, bold=False): def main(): parser = argparse.ArgumentParser(description='Duplicate and push PR between formplayer/master') - parser.add_argument('orig_pr_id', type=str, help="ID of PR to be cloned") parser.add_argument('orig_source_branch', type=str, help="Branch name of PR to be duplicated") parser.add_argument('orig_target_branch', type=str, help="Name of branch the original PR merged into", choices = [key.value for key in BranchName]) @@ -107,8 +107,8 @@ def main(): print("Creating branch {} from {}".format(new_source_branch, new_target_branch)) git_create_branch(orig_branch_name=new_target_branch, new_branch_name=new_source_branch) - print("Pulling {}".format(args.orig_source_branch)) - git_pull_pr(args.orig_pr_id, args.orig_source_branch) + print("Fetching {}".format(args.orig_source_branch)) + git_fetch_pr_branch(args.orig_source_branch) print("Getting new commits from {}".format(args.orig_source_branch)) new_commits = get_new_commits(args.orig_target_branch, args.orig_source_branch) From 5c466b73ea3281ae01abb7084fc8cca34168f0ad Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Fri, 31 Mar 2023 12:10:18 -0700 Subject: [PATCH 065/402] feat: output of git push --- scripts/duplicate_pr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 77c30214b4..0173cdf9d5 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -67,7 +67,8 @@ def cherry_pick_new_commits(commits:list[str], branch:str, git=None): def git_push_pr(branch:str, git=None): git = git or get_git() - git.push("origin", branch) + output = git.push("origin", branch, _err_to_out=True) + print(output) def merge_base_commit(branch1: str, branch2:str, git=None): From 370cbba2cb2e497e3aa117512f8dde7ad8378354 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Fri, 31 Mar 2023 12:13:21 -0700 Subject: [PATCH 066/402] refactor: remove unused code --- scripts/duplicate_pr.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 0173cdf9d5..97ac4153a1 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -9,8 +9,8 @@ class BranchName(Enum): FORMPLAYER = "formplayer" -def get_git(path=None): - return sh.git.bake(_tty_out=False, _cwd=path) +def get_git(): + return sh.git.bake(_tty_out=False) def get_target_branch(orig_target_branch:str): @@ -23,8 +23,8 @@ def get_target_branch(orig_target_branch:str): exit(1) -def git_create_branch(orig_branch_name:str, new_branch_name: str, git=None): - git = git or get_git() +def git_create_branch(orig_branch_name:str, new_branch_name: str): + git = get_git() try: git.checkout(orig_branch_name) except sh.ErrorReturnCode_1 as e: @@ -37,8 +37,8 @@ def git_create_branch(orig_branch_name:str, new_branch_name: str, git=None): exit(1) -def git_fetch_pr_branch(branch_name:str, git=None): - git = git or get_git() +def git_fetch_pr_branch(branch_name:str): + git = get_git() # fetch remote branch without switching branches input = "{0}:{0}".format(branch_name) try: @@ -48,8 +48,8 @@ def git_fetch_pr_branch(branch_name:str, git=None): exit(1) -def get_new_commits(base_branch: str, curr_branch:str, git=None): - git = git or get_git() +def get_new_commits(base_branch: str, curr_branch:str): + git = get_git() base_commit = merge_base_commit(base_branch, curr_branch) recent_commit = latest_commit(curr_branch) @@ -58,26 +58,26 @@ def get_new_commits(base_branch: str, curr_branch:str, git=None): return interested_commits -def cherry_pick_new_commits(commits:list[str], branch:str, git=None): - git = git or get_git() +def cherry_pick_new_commits(commits:list[str], branch:str): + git = get_git() git.checkout(branch) for commits in reversed(commits): git("cherry-pick", commits) -def git_push_pr(branch:str, git=None): - git = git or get_git() +def git_push_pr(branch:str): + git = get_git() output = git.push("origin", branch, _err_to_out=True) print(output) -def merge_base_commit(branch1: str, branch2:str, git=None): - git = git or get_git() +def merge_base_commit(branch1: str, branch2:str): + git = get_git() return str(git("merge-base", branch1, branch2).replace("\n", "")) -def latest_commit(branch:str, git=None): - git = git or get_git() +def latest_commit(branch:str): + git = get_git() return str(git("rev-parse", branch).replace("\n", "")) @@ -115,7 +115,7 @@ def main(): new_commits = get_new_commits(args.orig_target_branch, args.orig_source_branch) print("Cherry-picking commits from {} to {}".format(args.orig_source_branch, new_source_branch)) - cherry_pick_new_commits (new_commits, new_source_branch) + cherry_pick_new_commits(new_commits, new_source_branch) print("Pushing {}".format(new_source_branch)) git_push_pr(new_source_branch) From e3bcd70018710bf4a26d786310babbee00f5832e Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Fri, 31 Mar 2023 12:24:30 -0700 Subject: [PATCH 067/402] bug: fetches branch before checking out in case branch does not exist --- scripts/duplicate_pr.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 97ac4153a1..422165918a 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -26,6 +26,7 @@ def get_target_branch(orig_target_branch:str): def git_create_branch(orig_branch_name:str, new_branch_name: str): git = get_git() try: + git_fetch_branch(orig_branch_name) git.checkout(orig_branch_name) except sh.ErrorReturnCode_1 as e: print(red(e.stderr.decode())) @@ -37,7 +38,7 @@ def git_create_branch(orig_branch_name:str, new_branch_name: str): exit(1) -def git_fetch_pr_branch(branch_name:str): +def git_fetch_branch(branch_name:str): git = get_git() # fetch remote branch without switching branches input = "{0}:{0}".format(branch_name) @@ -109,7 +110,7 @@ def main(): git_create_branch(orig_branch_name=new_target_branch, new_branch_name=new_source_branch) print("Fetching {}".format(args.orig_source_branch)) - git_fetch_pr_branch(args.orig_source_branch) + git_fetch_branch(args.orig_source_branch) print("Getting new commits from {}".format(args.orig_source_branch)) new_commits = get_new_commits(args.orig_target_branch, args.orig_source_branch) From 6ef9e371408d8b1d52dbd1a620ca3881d7f151eb Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Sat, 8 Apr 2023 21:57:52 -0700 Subject: [PATCH 068/402] creates yml file and calls duplicate_pr script --- .github/workflows/duplicate_pr.yml | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/duplicate_pr.yml diff --git a/.github/workflows/duplicate_pr.yml b/.github/workflows/duplicate_pr.yml new file mode 100644 index 0000000000..7174c55ba1 --- /dev/null +++ b/.github/workflows/duplicate_pr.yml @@ -0,0 +1,32 @@ +name: Duplicate PR + +#Duplication workflow must be run before PR branch is deleted +on: + issue_comment: + types: [created, edited] + +jobs: + duplicate_pr: + name: Duplicate PR + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body,'@dimagibot duplicate this PR') }} + runs-on: ubuntu-latest + steps: + # https://github.com/marketplace/actions/pull-request-comment-branch + - name: Pull Request Comment Branch + uses: xt0rted/pull-request-comment-branch@v2.0.0 + id: comment-branch + - uses: actions/checkout@v3 + with: + submodules: recursive + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: '3.9' + - name: Install Requirements + run: pip install sh + - name: Config Git + run: | + git config --global user.email github-actions@github.com + git config --global user.name github-actions + - name: Run duplicate_pr.py script with PR not merged + run: python scripts/duplicate_pr.py ${{ steps.comment-branch.outputs.head_ref }} ${{ steps.comment-branch.outputs.base_ref }} From b9e1612bfffd2070156f78322f6a2e4e339f92af Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Sat, 8 Apr 2023 21:58:41 -0700 Subject: [PATCH 069/402] creates PR with different base branches depending on initial PR base branch --- .github/workflows/duplicate_pr.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/duplicate_pr.yml b/.github/workflows/duplicate_pr.yml index 7174c55ba1..2f6305ae43 100644 --- a/.github/workflows/duplicate_pr.yml +++ b/.github/workflows/duplicate_pr.yml @@ -30,3 +30,31 @@ jobs: git config --global user.name github-actions - name: Run duplicate_pr.py script with PR not merged run: python scripts/duplicate_pr.py ${{ steps.comment-branch.outputs.head_ref }} ${{ steps.comment-branch.outputs.base_ref }} + - name: Create Pull Request Base Master + if: ${{ steps.comment-branch.outputs.base_ref == 'formplayer' }} + run: gh pr create --title "${title_text}" --body "${body_text}" --base master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + title_text: Duplicate of ${{ steps.comment-branch.outputs.head_ref }} + body_text: | + Copy of PR ${{github.event.issue.pull_request.html_url}} and used GitHub CLI to open this pull request. + + GitHub Actions automatically ran duplicate_pr.py + + Please **close and reopen** this pull request to have tests run. + + Workaround for [this GitHub Actions constraint](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow) + - name: Create Pull Request Base Formplayer + if: ${{ steps.comment-branch.outputs.base_ref == 'master' }} + run: gh pr create --title "${title_text}" --body "${body_text}" --base formplayer + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + title_text: Duplicate of ${{ steps.comment-branch.outputs.head_ref }} + body_text: | + Copy of PR ${{github.event.issue.pull_request.html_url}} + + GitHub Actions automatically ran duplicate_pr.py and used GitHub CLI to open this pull request. + + Please **close and reopen** this pull request to have tests run. + + Workaround for [this GitHub Actions constraint](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow) From 54cd8bb259ada05b367c2c30a1897554bc52e2e3 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Sat, 8 Apr 2023 22:01:41 -0700 Subject: [PATCH 070/402] bug: error occurs if tries to fetch master branch --- scripts/duplicate_pr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 422165918a..37c6f5d768 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -26,7 +26,8 @@ def get_target_branch(orig_target_branch:str): def git_create_branch(orig_branch_name:str, new_branch_name: str): git = get_git() try: - git_fetch_branch(orig_branch_name) + if orig_branch_name != BranchName.MASTER.value: + git_fetch_branch(orig_branch_name) git.checkout(orig_branch_name) except sh.ErrorReturnCode_1 as e: print(red(e.stderr.decode())) From 40aecd44bc989b760740e0f1194dfc40309ac619 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Sat, 8 Apr 2023 22:02:35 -0700 Subject: [PATCH 071/402] bug: base_branch (formplayer) is needs to be fetched --- scripts/duplicate_pr.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 37c6f5d768..5fe95c99a9 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -52,6 +52,8 @@ def git_fetch_branch(branch_name:str): def get_new_commits(base_branch: str, curr_branch:str): git = get_git() + if base_branch != BranchName.MASTER.value: + git_fetch_branch(base_branch) base_commit = merge_base_commit(base_branch, curr_branch) recent_commit = latest_commit(curr_branch) From 8e81d32d8fe69b49ef1ab990116cdf2abb24d0bb Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Sat, 8 Apr 2023 22:05:34 -0700 Subject: [PATCH 072/402] feat: merge-base can't find base commit if PR is already merged. Initial SHA argument is used to define commit to start cherrypicking from --- scripts/duplicate_pr.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 5fe95c99a9..ff7b44f5d3 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -50,11 +50,14 @@ def git_fetch_branch(branch_name:str): exit(1) -def get_new_commits(base_branch: str, curr_branch:str): +def get_new_commits(base_branch: str, curr_branch:str, base_commit:str = None): git = get_git() if base_branch != BranchName.MASTER.value: git_fetch_branch(base_branch) - base_commit = merge_base_commit(base_branch, curr_branch) + if base_commit: + base_commit = git.show("{}^1".format(base_commit)).split()[1] + else: + base_commit = merge_base_commit(base_branch, curr_branch) recent_commit = latest_commit(curr_branch) commits_range = "{}..{}".format(base_commit, recent_commit) @@ -104,6 +107,7 @@ def main(): parser.add_argument('orig_source_branch', type=str, help="Branch name of PR to be duplicated") parser.add_argument('orig_target_branch', type=str, help="Name of branch the original PR merged into", choices = [key.value for key in BranchName]) + parser.add_argument('-i','--initial_sha', type=str, help="SHA of first commit in PR to be duplicated") args = parser.parse_args() new_source_branch = "copy_of_" + args.orig_source_branch @@ -116,7 +120,7 @@ def main(): git_fetch_branch(args.orig_source_branch) print("Getting new commits from {}".format(args.orig_source_branch)) - new_commits = get_new_commits(args.orig_target_branch, args.orig_source_branch) + new_commits = get_new_commits(args.orig_target_branch, args.orig_source_branch, args.initial_sha) print("Cherry-picking commits from {} to {}".format(args.orig_source_branch, new_source_branch)) cherry_pick_new_commits(new_commits, new_source_branch) From 5a209f3ad0d6a845abf82a64f7d61ecadad532fb Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Sat, 8 Apr 2023 22:07:23 -0700 Subject: [PATCH 073/402] feat: gets intiial-sha from comment to pass into script for PRs that are merged --- .github/workflows/duplicate_pr.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/duplicate_pr.yml b/.github/workflows/duplicate_pr.yml index 2f6305ae43..68d9b1b0ad 100644 --- a/.github/workflows/duplicate_pr.yml +++ b/.github/workflows/duplicate_pr.yml @@ -11,6 +11,15 @@ jobs: if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body,'@dimagibot duplicate this PR') }} runs-on: ubuntu-latest steps: + # https://github.com/marketplace/actions/github-script + - name: Get PR Intial Commit SHA + if: ${{ github.event.issue.pull_request.merged_at }} + id: initial-sha + uses: actions/github-script@0.9.0 + with: + script: | + const sha = context.payload.comment.body.split(" ").pop() + core.setOutput('sha', sha) # https://github.com/marketplace/actions/pull-request-comment-branch - name: Pull Request Comment Branch uses: xt0rted/pull-request-comment-branch@v2.0.0 @@ -28,7 +37,15 @@ jobs: run: | git config --global user.email github-actions@github.com git config --global user.name github-actions + - name: Run duplicate_pr.py script with PR merged + if: ${{ github.event.issue.pull_request.merged_at }} + run: | + echo Intial SHA is "${{ steps.initial-sha.outputs.sha }}" + python scripts/duplicate_pr.py ${{ steps.comment-branch.outputs.head_ref }} ${{ steps.comment-branch.outputs.base_ref }} --initial_sha ${initial_sha} + env: + initial_sha: ${{ steps.initial-sha.outputs.sha }} - name: Run duplicate_pr.py script with PR not merged + if: ${{ github.event.issue.pull_request.merged_at == null }} run: python scripts/duplicate_pr.py ${{ steps.comment-branch.outputs.head_ref }} ${{ steps.comment-branch.outputs.base_ref }} - name: Create Pull Request Base Master if: ${{ steps.comment-branch.outputs.base_ref == 'formplayer' }} From bc5bd3d50bdd95e0ae335915de2eaaa060cf8b75 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Sat, 8 Apr 2023 22:08:28 -0700 Subject: [PATCH 074/402] bug: handles scenario where cherry-pick commit is empty --- scripts/duplicate_pr.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index ff7b44f5d3..f82bcb855e 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -69,7 +69,12 @@ def cherry_pick_new_commits(commits:list[str], branch:str): git = get_git() git.checkout(branch) for commits in reversed(commits): - git("cherry-pick", commits) + try: + empty_commit_message = "The previous cherry-pick is now empty" + git("cherry-pick", commits) + except sh.ErrorReturnCode_1 as e: + if empty_commit_message in e.stderr.decode(): + git("cherry-pick", "--skip") def git_push_pr(branch:str): From f0cd61179e9f95abe3e94ede46c2fc475d10b468 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Mon, 10 Apr 2023 09:50:29 -0700 Subject: [PATCH 075/402] simplify command to trigger action --- .github/workflows/duplicate_pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/duplicate_pr.yml b/.github/workflows/duplicate_pr.yml index 68d9b1b0ad..7a1c899fb1 100644 --- a/.github/workflows/duplicate_pr.yml +++ b/.github/workflows/duplicate_pr.yml @@ -8,7 +8,7 @@ on: jobs: duplicate_pr: name: Duplicate PR - if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body,'@dimagibot duplicate this PR') }} + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body,'duplicate this PR') }} runs-on: ubuntu-latest steps: # https://github.com/marketplace/actions/github-script From 4f9ba89fba955f53e9f6a41711038b0d729b48f6 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Mon, 10 Apr 2023 11:01:36 -0700 Subject: [PATCH 076/402] refactor: DRY --- .github/workflows/duplicate_pr.yml | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/.github/workflows/duplicate_pr.yml b/.github/workflows/duplicate_pr.yml index 7a1c899fb1..47a7c022af 100644 --- a/.github/workflows/duplicate_pr.yml +++ b/.github/workflows/duplicate_pr.yml @@ -47,24 +47,18 @@ jobs: - name: Run duplicate_pr.py script with PR not merged if: ${{ github.event.issue.pull_request.merged_at == null }} run: python scripts/duplicate_pr.py ${{ steps.comment-branch.outputs.head_ref }} ${{ steps.comment-branch.outputs.base_ref }} - - name: Create Pull Request Base Master - if: ${{ steps.comment-branch.outputs.base_ref == 'formplayer' }} - run: gh pr create --title "${title_text}" --body "${body_text}" --base master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - title_text: Duplicate of ${{ steps.comment-branch.outputs.head_ref }} - body_text: | - Copy of PR ${{github.event.issue.pull_request.html_url}} and used GitHub CLI to open this pull request. - - GitHub Actions automatically ran duplicate_pr.py - - Please **close and reopen** this pull request to have tests run. - - Workaround for [this GitHub Actions constraint](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow) - - name: Create Pull Request Base Formplayer - if: ${{ steps.comment-branch.outputs.base_ref == 'master' }} - run: gh pr create --title "${title_text}" --body "${body_text}" --base formplayer + - name: Determine Base Branch + id: base-branch + run: | + if [[ ${{ steps.comment-branch.outputs.base_ref}} = "formplayer" ]] + then echo "NAME=master" >> $GITHUB_OUTPUT + elif [[ ${{ steps.comment-branch.outputs.base_ref}} = "master" ]] + then echo "NAME=formplayer" >> $GITHUB_OUTPUT + fi + - name: Create Pull Request + run: gh pr create --title "${title_text}" --body "${body_text}" --base "${base_branch_name}" env: + base_branch_name: ${{ steps.base-branch.outputs.NAME }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} title_text: Duplicate of ${{ steps.comment-branch.outputs.head_ref }} body_text: | From c86fd784eb1828752667b8243518f7cf4d6dcc03 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Mon, 10 Apr 2023 11:35:12 -0700 Subject: [PATCH 077/402] adds instructions for using GitHub Action --- .github/contributing.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/contributing.md b/.github/contributing.md index bfd957eab0..b2c9102d02 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -8,9 +8,15 @@ We try to keep both these branches in sync with each other the best we can. To d 1. If you are working on a Formplayer change, you will want to start by checking out `your_feature_branch` from `formplayer` as the base branch. Make changes on `your_feature_branch` and create your original PR against `formplayer` branch. -2. Now you will need to duplicate this PR by making another PR against `master`. To do this, firstly you would want to compare `your_feature_branch` to the `master` and see if the diff only shows the commit that belongs to your branch. If yes, you can directly create another PR from `your_feature_branch` to `master`. -Otherwise you will need to create another branch by checking out `your_feature_branch_dupe` from `master` as the base branch. You will then need to [cherry-pick](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/cherry-picking-a-commit#about-git-cherry-pick) each of the commits from `your_feature_branch` to `your_feature_branch_dupe`. -Subsequently you should create a PR from `your_feature_branch_dupe` against `master`. While creating this PR you can remove all the PR template field and simply mention your original `formplayer` PR as a reference. +2. Now you will need to duplicate this PR by making another PR against `master`. + + To do this with GitHub Action - + Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. + + To do this manually - + Firstly you would want to compare `your_feature_branch` to the `master` and see if the diff only shows the commit that belongs to your branch. If yes, you can directly create another PR from `your_feature_branch` to `master`. + Otherwise you will need to create another branch by checking out `your_feature_branch_dupe` from `master` as the base branch. You will then need to [cherry-pick](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/cherry-picking-a-commit#about-git-cherry-pick) each of the commits from `your_feature_branch` to `your_feature_branch_dupe`. + Subsequently you should create a PR from `your_feature_branch_dupe` against `master`. While creating this PR you can remove all the PR template field and simply mention your original `formplayer` PR as a reference. 3. In order for us to test that your PR against `master` doesn't break anything on CommCare Android, we need to run android side tests with your PR. To do this - @@ -31,9 +37,15 @@ To do this - 1. If you are working on a CommCare Android change, you will want to start by checking out `your_feature_branch` from `master` as the base branch. Make changes on `your_feature_branch` and create your original PR against `master` branch. -2. Now you will need to duplicate this PR by making another PR against `formplayer`. To do this, firstly you would want to compare `your_feature_branch` to the `formplayer` and see if the diff only shows the commit that belongs to your branch. If yes, you can directly create another PR from `your_feature_branch` to `formplayer`. -Otherwise you will need to create another branch by checking out `your_feature_branch_dupe` from `formplayer` as the base branch. You will then need to [cherry-pick](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/cherry-picking-a-commit#about-git-cherry-pick) each of the commits from `your_feature_branch` to `your_feature_branch_dupe`. -Subsequently you should create a PR from `your_feature_branch_dupe` against `formplayer`. While creating this PR you can remove all the PR template field and simply mention your original `master` PR as a reference. +2. Now you will need to duplicate this PR by making another PR against `formplayer`. + + To do this with GitHub Action - + Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. + + To do this manually - + Firstly you would want to compare `your_feature_branch` to the `formplayer` and see if the diff only shows the commit that belongs to your branch. If yes, you can directly create another PR from `your_feature_branch` to `formplayer`. + Otherwise you will need to create another branch by checking out `your_feature_branch_dupe` from `formplayer` as the base branch. You will then need to [cherry-pick](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/cherry-picking-a-commit#about-git-cherry-pick) each of the commits from `your_feature_branch` to `your_feature_branch_dupe`. + Subsequently you should create a PR from `your_feature_branch_dupe` against `formplayer`. While creating this PR you can remove all the PR template field and simply mention your original `master` PR as a reference. 3. In order for us to test that your PR against `formplayer` doesn't break anything on Formplayer, we need to run formplayer side tests with your PR. To do this - From 325c44baef37969d33d556f07b45b107442f50cd Mon Sep 17 00:00:00 2001 From: Jonathan Tang <88759246+Jtang-1@users.noreply.github.com> Date: Mon, 10 Apr 2023 13:46:00 -0700 Subject: [PATCH 078/402] simplify instructions --- .github/contributing.md | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/.github/contributing.md b/.github/contributing.md index b2c9102d02..817abeaf97 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -8,15 +8,7 @@ We try to keep both these branches in sync with each other the best we can. To d 1. If you are working on a Formplayer change, you will want to start by checking out `your_feature_branch` from `formplayer` as the base branch. Make changes on `your_feature_branch` and create your original PR against `formplayer` branch. -2. Now you will need to duplicate this PR by making another PR against `master`. - - To do this with GitHub Action - - Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. - - To do this manually - - Firstly you would want to compare `your_feature_branch` to the `master` and see if the diff only shows the commit that belongs to your branch. If yes, you can directly create another PR from `your_feature_branch` to `master`. - Otherwise you will need to create another branch by checking out `your_feature_branch_dupe` from `master` as the base branch. You will then need to [cherry-pick](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/cherry-picking-a-commit#about-git-cherry-pick) each of the commits from `your_feature_branch` to `your_feature_branch_dupe`. - Subsequently you should create a PR from `your_feature_branch_dupe` against `master`. While creating this PR you can remove all the PR template field and simply mention your original `formplayer` PR as a reference. +2. Now you will need to duplicate this PR by making another PR against `master`. Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. 3. In order for us to test that your PR against `master` doesn't break anything on CommCare Android, we need to run android side tests with your PR. To do this - @@ -37,15 +29,7 @@ To do this - 1. If you are working on a CommCare Android change, you will want to start by checking out `your_feature_branch` from `master` as the base branch. Make changes on `your_feature_branch` and create your original PR against `master` branch. -2. Now you will need to duplicate this PR by making another PR against `formplayer`. - - To do this with GitHub Action - - Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. - - To do this manually - - Firstly you would want to compare `your_feature_branch` to the `formplayer` and see if the diff only shows the commit that belongs to your branch. If yes, you can directly create another PR from `your_feature_branch` to `formplayer`. - Otherwise you will need to create another branch by checking out `your_feature_branch_dupe` from `formplayer` as the base branch. You will then need to [cherry-pick](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/cherry-picking-a-commit#about-git-cherry-pick) each of the commits from `your_feature_branch` to `your_feature_branch_dupe`. - Subsequently you should create a PR from `your_feature_branch_dupe` against `formplayer`. While creating this PR you can remove all the PR template field and simply mention your original `master` PR as a reference. +2. Now you will need to duplicate this PR by making another PR against `formplayer`. Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. 3. In order for us to test that your PR against `formplayer` doesn't break anything on Formplayer, we need to run formplayer side tests with your PR. To do this - From 2844db63f778ecf7bbe18ab6d03b3607efdf5aa0 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Mon, 10 Apr 2023 21:54:55 -0700 Subject: [PATCH 079/402] feat: helpful error message upon push failure --- scripts/duplicate_pr.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index f82bcb855e..6dfdf4df45 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -79,7 +79,10 @@ def cherry_pick_new_commits(commits:list[str], branch:str): def git_push_pr(branch:str): git = get_git() - output = git.push("origin", branch, _err_to_out=True) + try: + output = git.push("origin", branch, _err_to_out=True) + except sh.ErrorReturnCode_1 as e: + print(red("Failed to push. Branch {} already exists remotely. Try deleting remote branch and run action again".format(branch))) print(output) From 4ab0aa412f0c717d632f7a4d14f130618803b2cc Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Tue, 18 Apr 2023 16:12:19 -0700 Subject: [PATCH 080/402] bug: if branch is merged, commit will have multiple parents causing commit traversal to pull in secondary parent commits. --- scripts/duplicate_pr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 6dfdf4df45..68a31f2ef8 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -61,7 +61,7 @@ def get_new_commits(base_branch: str, curr_branch:str, base_commit:str = None): recent_commit = latest_commit(curr_branch) commits_range = "{}..{}".format(base_commit, recent_commit) - interested_commits = git("rev-list", "--no-merges", commits_range).split() + interested_commits = git("rev-list", "--no-merges", "--first-parent", commits_range).split() return interested_commits From f636437c4d5f37b98bbf282aff8a798005453345 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 20 Mar 2023 17:18:55 -0300 Subject: [PATCH 081/402] Adds storage init and exists method to be used for case search storage --- .../services/storage/IStorageUtilityIndexed.java | 11 +++++++++++ .../storage/util/DummyIndexedStorageUtility.java | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java b/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java index 3e3bd0643b..7501c5d951 100644 --- a/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java +++ b/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java @@ -272,4 +272,15 @@ void bulkRead(LinkedHashSet cuedCases, HashMap recordMap) * Provide public accessor to the inner class that is stored */ Class getPrototype(); + + /** + * if the storage exists or not + * @return a boolean indicating if the storage exists + */ + boolean isStorageExists(); + + /** + * initialise the storage, for example create the table in the DB for this storage + */ + void initStorage(); } diff --git a/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java b/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java index 177cc59821..c39d16fe2e 100644 --- a/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java +++ b/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java @@ -371,4 +371,16 @@ public void bulkReadMetadata(LinkedHashSet cuedCases, String[] metaDataIds, Hash public Class getPrototype() { return prototype; } + + @Override + public boolean isStorageExists() { + // this method is not really meaningful for dummy storage + // so default to returning true until needed otherwise + return true; + } + + @Override + public void initStorage() { + // do nothing as storage is initialized by default + } } From ffda9234a2725e6c7706ccb6163e8150516fe142 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 20 Mar 2023 17:27:51 -0300 Subject: [PATCH 082/402] Handle CaseInstanceTreeElement as a instance root --- .../org/javarosa/core/model/instance/utils/InstanceUtils.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java index 83c4367a82..763de62565 100644 --- a/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java +++ b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java @@ -2,6 +2,7 @@ import static org.javarosa.core.model.instance.utils.TreeUtilities.xmlToTreeElement; +import org.commcare.cases.instance.CaseInstanceTreeElement; import org.javarosa.core.model.instance.AbstractTreeElement; import org.javarosa.core.model.instance.FormInstance; import org.javarosa.core.model.instance.InstanceBase; @@ -37,6 +38,9 @@ public static void setUpInstanceRoot(AbstractTreeElement instanceRoot, String in TreeElement rootAsTreeElement = ((TreeElement)instanceRoot); rootAsTreeElement.setInstanceName(instanceId); rootAsTreeElement.setParent(instanceBase); + } else if (instanceRoot instanceof CaseInstanceTreeElement) { + CaseInstanceTreeElement caseInstanceRoot = ((CaseInstanceTreeElement)instanceRoot); + caseInstanceRoot.rebase(instanceBase); } else { String error = "Unrecognised Instance root of type " + instanceRoot.getClass().getName() + " for instance " + instanceId; From 208d79ef92eec33a9bc49cd126fa9d8cbb3b13d0 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 20 Mar 2023 17:29:16 -0300 Subject: [PATCH 083/402] change method param type to interface type --- src/main/java/org/commcare/core/parse/ParseUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/commcare/core/parse/ParseUtils.java b/src/main/java/org/commcare/core/parse/ParseUtils.java index 598a8467a9..75f73c7907 100644 --- a/src/main/java/org/commcare/core/parse/ParseUtils.java +++ b/src/main/java/org/commcare/core/parse/ParseUtils.java @@ -2,6 +2,7 @@ import org.commcare.core.interfaces.UserSandbox; import org.commcare.data.xml.DataModelPullParser; +import org.commcare.data.xml.TransactionParserFactory; import org.javarosa.xml.util.InvalidStructureException; import org.javarosa.xml.util.UnfullfilledRequirementsException; import org.xmlpull.v1.XmlPullParserException; @@ -31,10 +32,9 @@ public static void parseIntoSandbox(InputStream stream, UserSandbox sandbox, boo parseIntoSandbox(stream, factory, failfast, bulkProcessingEnabled); } - public static void parseIntoSandbox(InputStream stream, CommCareTransactionParserFactory factory, + public static void parseIntoSandbox(InputStream stream, TransactionParserFactory factory, boolean failfast, boolean bulkProcessingEnabled) throws IOException, InvalidStructureException, UnfullfilledRequirementsException, XmlPullParserException { DataModelPullParser parser = new DataModelPullParser(stream, factory, failfast, bulkProcessingEnabled); parser.parse(); - } } From ec933497aaefbc5ce04a8fd5dcb1ebb166a2c13d Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 22 Mar 2023 12:01:10 -0300 Subject: [PATCH 084/402] refactor existing parse tests to separate out the static code --- .../cases/test/CaseParseAndReadTest.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/test/java/org/commcare/cases/test/CaseParseAndReadTest.java b/src/test/java/org/commcare/cases/test/CaseParseAndReadTest.java index 05a6231f8f..66ea7bef85 100644 --- a/src/test/java/org/commcare/cases/test/CaseParseAndReadTest.java +++ b/src/test/java/org/commcare/cases/test/CaseParseAndReadTest.java @@ -53,7 +53,7 @@ public void setUp() { @Test public void testReadCaseDB() throws Exception { - compareCaseDbState("/case_create_basic.xml", "/case_create_basic_output.xml"); + parseAndCompareCaseDbState("/case_create_basic.xml", "/case_create_basic_output.xml"); EvaluationContext ec = MockDataUtils.buildContextWithInstance(this.sandbox, "casedb", CaseTestUtils.CASE_INSTANCE); @@ -62,21 +62,23 @@ public void testReadCaseDB() throws Exception { @Test public void testDoubleCreateCaseWithUpdate() throws Exception { - compareCaseDbState("/case_create_overwrite.xml", "/case_create_overwrite_output.xml"); + parseAndCompareCaseDbState("/case_create_overwrite.xml", "/case_create_overwrite_output.xml"); EvaluationContext ec = MockDataUtils.buildContextWithInstance(this.sandbox, "casedb", CaseTestUtils.CASE_INSTANCE); Assert.assertTrue(CaseTestUtils.xpathEvalAndCompare(ec, "instance('casedb')/casedb/case[@case_id = 'case_one']/case_name", "case_overwrite")); Assert.assertTrue(CaseTestUtils.xpathEvalAndCompare(ec, "instance('casedb')/casedb/case[@case_id = 'case_one']/case_property1", "one")); Assert.assertTrue(CaseTestUtils.xpathEvalAndCompare(ec, "instance('casedb')/casedb/case[@case_id = 'case_one']/case_property2", "property_two")); } - - - private void compareCaseDbState(String inputTransactions, + private void parseAndCompareCaseDbState(String inputTransactions, String caseDbState) throws Exception { config.parseIntoSandbox(this.getClass().getResourceAsStream(inputTransactions), sandbox, false); + compareCaseDbState(sandbox, getClass().getResourceAsStream(caseDbState)); + } + private static void compareCaseDbState(MockUserDataSandbox sandbox, InputStream caseDbState) + throws IOException { byte[] parsedDb = serializeCaseInstanceFromSandbox(sandbox); Document parsed = XmlComparator.getDocumentFromStream(new ByteArrayInputStream(parsedDb)); - Document loaded = XmlComparator.getDocumentFromStream(this.getClass().getResourceAsStream(caseDbState)); + Document loaded = XmlComparator.getDocumentFromStream(caseDbState); try { XmlComparator.isDOMEqual(parsed, loaded); @@ -104,12 +106,9 @@ private static byte[] serializeCaseInstanceFromSandbox(MockUserDataSandbox sandb } - private byte[] dumpStream(String inputResource) throws IOException { - InputStream is = this.getClass().getResourceAsStream(inputResource); + private static byte[] dumpStream(InputStream is) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); - StreamsUtil.writeFromInputToOutput(is, bos); - return bos.toByteArray(); } } From 7dfa8f86f9be25f792ff7a1607f708796f67a28f Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 22 Mar 2023 12:02:26 -0300 Subject: [PATCH 085/402] move helpful parse and compare method to utils class to be able to reuse it elsewhere --- .../cases/test/CaseParseAndReadTest.java | 40 +------------ .../test/utilities/CaseTestUtils.java | 59 +++++++++++++++++++ 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/test/java/org/commcare/cases/test/CaseParseAndReadTest.java b/src/test/java/org/commcare/cases/test/CaseParseAndReadTest.java index 66ea7bef85..e6cfdcf49f 100644 --- a/src/test/java/org/commcare/cases/test/CaseParseAndReadTest.java +++ b/src/test/java/org/commcare/cases/test/CaseParseAndReadTest.java @@ -1,5 +1,7 @@ package org.commcare.cases.test; +import static org.commcare.test.utilities.CaseTestUtils.compareCaseDbState; + import org.commcare.cases.instance.CaseInstanceTreeElement; import org.commcare.test.utilities.CaseTestUtils; import org.commcare.test.utilities.TestInstanceInitializer; @@ -73,42 +75,4 @@ private void parseAndCompareCaseDbState(String inputTransactions, config.parseIntoSandbox(this.getClass().getResourceAsStream(inputTransactions), sandbox, false); compareCaseDbState(sandbox, getClass().getResourceAsStream(caseDbState)); } - - private static void compareCaseDbState(MockUserDataSandbox sandbox, InputStream caseDbState) - throws IOException { - byte[] parsedDb = serializeCaseInstanceFromSandbox(sandbox); - Document parsed = XmlComparator.getDocumentFromStream(new ByteArrayInputStream(parsedDb)); - Document loaded = XmlComparator.getDocumentFromStream(caseDbState); - - try { - XmlComparator.isDOMEqual(parsed, loaded); - } catch(Exception e) { - System.out.print(new String(parsedDb)); - - //NOTE: The DOM's definitely don't match here, so the strings cannot be the same. - //The reason we are asserting equality is because the delta between the strings is - //likely to do a good job of contextualizing where the DOM's don't match. - Assert.assertEquals("CaseDB output did not match expected structure(" + e.getMessage() + ")", new String(dumpStream(caseDbState)), new String(parsedDb)); - } - } - - private static byte[] serializeCaseInstanceFromSandbox(MockUserDataSandbox sandbox) { - try { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - DataModelSerializer s = new DataModelSerializer(bos, new TestInstanceInitializer(sandbox)); - - s.serialize(new ExternalDataInstance(CaseTestUtils.CASE_INSTANCE, CaseInstanceTreeElement.MODEL_NAME), null); - - return bos.toByteArray(); - } catch (IOException e) { - throw new RuntimeException(e.getMessage()); - } - } - - - private static byte[] dumpStream(InputStream is) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - StreamsUtil.writeFromInputToOutput(is, bos); - return bos.toByteArray(); - } } diff --git a/src/test/java/org/commcare/test/utilities/CaseTestUtils.java b/src/test/java/org/commcare/test/utilities/CaseTestUtils.java index 51557c48ab..19c2b72acd 100644 --- a/src/test/java/org/commcare/test/utilities/CaseTestUtils.java +++ b/src/test/java/org/commcare/test/utilities/CaseTestUtils.java @@ -1,11 +1,22 @@ package org.commcare.test.utilities; +import org.commcare.cases.instance.CaseInstanceTreeElement; +import org.commcare.util.mocks.MockUserDataSandbox; +import org.javarosa.core.io.StreamsUtil; import org.javarosa.core.model.condition.EvaluationContext; +import org.javarosa.core.model.instance.ExternalDataInstance; +import org.javarosa.model.xform.DataModelSerializer; import org.javarosa.xpath.XPathParseTool; import org.javarosa.xpath.expr.FunctionUtils; import org.javarosa.xpath.expr.XPathExpression; import org.javarosa.xpath.parser.XPathSyntaxException; import org.junit.Assert; +import org.kxml2.kdom.Document; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; public class CaseTestUtils { public static final String CASE_INSTANCE = "jr://instance/casedb"; @@ -38,4 +49,52 @@ public static Object xpathEval(EvaluationContext evalContext, XPathExpression expr = XPathParseTool.parseXPath(input); return FunctionUtils.unpack(expr.eval(evalContext)); } + + + /** + * Compares the case db state in a sandbox with the given data + * + * @param sandbox Sandbox with case data + * @param caseDbState expected state for casedb instance + * @throws IOException + */ + public static void compareCaseDbState(MockUserDataSandbox sandbox, InputStream caseDbState) + throws IOException { + byte[] parsedDb = serializeCaseInstanceFromSandbox(sandbox); + Document parsed = XmlComparator.getDocumentFromStream(new ByteArrayInputStream(parsedDb)); + Document loaded = XmlComparator.getDocumentFromStream(caseDbState); + + try { + XmlComparator.isDOMEqual(parsed, loaded); + } catch (Exception e) { + System.out.print(new String(parsedDb)); + + //NOTE: The DOM's definitely don't match here, so the strings cannot be the same. + //The reason we are asserting equality is because the delta between the strings is + //likely to do a good job of contextualizing where the DOM's don't match. + Assert.assertEquals("CaseDB output did not match expected structure(" + e.getMessage() + ")", + new String(dumpStream(caseDbState)), new String(parsedDb)); + } + } + + private static byte[] serializeCaseInstanceFromSandbox(MockUserDataSandbox sandbox) { + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataModelSerializer s = new DataModelSerializer(bos, new TestInstanceInitializer(sandbox)); + + s.serialize(new ExternalDataInstance(CaseTestUtils.CASE_INSTANCE, CaseInstanceTreeElement.MODEL_NAME), + null); + + return bos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + } + + + private static byte[] dumpStream(InputStream is) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + StreamsUtil.writeFromInputToOutput(is, bos); + return bos.toByteArray(); + } } From d40e94e31915c683e4c630962c286aad46e4c630 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 22 Mar 2023 14:25:11 -0300 Subject: [PATCH 086/402] add bulk match method to storage interface --- .../core/services/storage/IStorageUtilityIndexed.java | 11 +++++++++++ .../storage/util/DummyIndexedStorageUtility.java | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java b/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java index 7501c5d951..aeb28f645b 100644 --- a/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java +++ b/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java @@ -4,6 +4,7 @@ import org.javarosa.core.util.InvalidIndexException; import org.javarosa.core.util.externalizable.Externalizable; +import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -268,6 +269,16 @@ void bulkRead(LinkedHashSet cuedCases, HashMap recordMap) */ void bulkReadMetadata(LinkedHashSet recordIds, String[] metaFieldNames, HashMap metadataMap); + + /** + * Bulk retrieves a set of the records in storage based on a list of values matching one of the + * field for this storage + * @param metaFieldName field we are matching against + * @param matchingValues matching values for metaFieldName that we want to filter records against + * @return A Vector of Externalizable objects e, such that the field specified is equal to the corresponding value provided. + */ + Vector getBulkRecordsForIndex(String metaFieldName, Collection matchingValues); + /** * Provide public accessor to the inner class that is stored */ diff --git a/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java b/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java index c39d16fe2e..b679e7a15d 100644 --- a/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java +++ b/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java @@ -18,6 +18,7 @@ import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; @@ -360,6 +361,12 @@ public String[] getMetaDataForRecord(int recordId, String[] fieldNames) { return response; } + @Override + public Vector getBulkRecordsForIndex(String metaFieldName, Collection matchingValues) { + // we don't care about bulk retrieval for dummy storage, so just call normal method to get records here + return getRecordsForValues(new String[]{metaFieldName}, matchingValues.toArray()); + } + @Override public void bulkReadMetadata(LinkedHashSet cuedCases, String[] metaDataIds, HashMap metadataMap) { for (int i : ((LinkedHashSet)cuedCases)) { From d6ced37a7b8e9c6e6e69b84f6987ebd937c803fb Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 22 Mar 2023 14:26:04 -0300 Subject: [PATCH 087/402] adds common case index methods to interface --- .../org/commcare/modern/engine/cases/CaseIndexTable.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/commcare/modern/engine/cases/CaseIndexTable.java b/src/main/java/org/commcare/modern/engine/cases/CaseIndexTable.java index b28151faeb..0570bf1759 100644 --- a/src/main/java/org/commcare/modern/engine/cases/CaseIndexTable.java +++ b/src/main/java/org/commcare/modern/engine/cases/CaseIndexTable.java @@ -1,5 +1,6 @@ package org.commcare.modern.engine.cases; +import org.commcare.cases.model.Case; import org.commcare.cases.query.queryset.DualTableSingleMatchModelQuerySet; import java.util.Collection; @@ -18,4 +19,8 @@ public interface CaseIndexTable { LinkedHashSet getCasesMatchingValueSet(String indexName, String[] valueSet); LinkedHashSet getCasesMatchingIndex(String indexName, String value); + + void indexCase(Case c); + + void clearCaseIndices(Collection idsToClear); } From f9d2e78ab0675e23259d5d1c3fbc3eefc6310f34 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 22 Mar 2023 14:59:04 -0300 Subject: [PATCH 088/402] implements case instance xml parser --- ...seInstanceXmlTransactionParserFactory.java | 48 ++++ .../xml/bulk/BulkCaseInstanceXmlParser.java | 227 ++++++++++++++++++ .../test/BulkCaseInstanceXmlParserTests.java | 52 ++++ .../case_instance_output.xml | 1 + .../case_instance_valid.xml | 22 ++ 5 files changed, 350 insertions(+) create mode 100644 src/main/java/org/commcare/core/parse/CaseInstanceXmlTransactionParserFactory.java create mode 100644 src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java create mode 100644 src/test/java/org/commcare/cases/util/test/BulkCaseInstanceXmlParserTests.java create mode 100644 src/test/resources/case_instance_parse/case_instance_output.xml create mode 100644 src/test/resources/case_instance_parse/case_instance_valid.xml diff --git a/src/main/java/org/commcare/core/parse/CaseInstanceXmlTransactionParserFactory.java b/src/main/java/org/commcare/core/parse/CaseInstanceXmlTransactionParserFactory.java new file mode 100644 index 0000000000..bfb5cc1856 --- /dev/null +++ b/src/main/java/org/commcare/core/parse/CaseInstanceXmlTransactionParserFactory.java @@ -0,0 +1,48 @@ +package org.commcare.core.parse; + +import org.commcare.core.interfaces.UserSandbox; +import org.commcare.data.xml.TransactionParser; +import org.commcare.data.xml.TransactionParserFactory; +import org.commcare.modern.engine.cases.CaseIndexTable; +import org.commcare.xml.bulk.BulkCaseInstanceXmlParser; +import org.kxml2.io.KXmlParser; + +public class CaseInstanceXmlTransactionParserFactory implements TransactionParserFactory { + + private final UserSandbox sandbox; + private final CaseIndexTable caseIndexTable; + private TransactionParserFactory caseParser; + + public CaseInstanceXmlTransactionParserFactory(UserSandbox sandbox, CaseIndexTable caseIndexTable) { + this.sandbox = sandbox; + this.caseIndexTable = caseIndexTable; + initCaseParser(); + } + + private void initCaseParser() { + caseParser = new TransactionParserFactory() { + BulkCaseInstanceXmlParser created = null; + + @Override + public TransactionParser getParser(KXmlParser parser) { + if (created == null) { + created = new BulkCaseInstanceXmlParser(parser, sandbox.getCaseStorage(), caseIndexTable); + } + + return created; + } + }; + } + + @Override + public TransactionParser getParser(KXmlParser parser) { + String name = parser.getName(); + if ("case".equalsIgnoreCase(name)) { + if (caseParser == null) { + throw new RuntimeException("Couldn't receive Case transaction without initialization!"); + } + return caseParser.getParser(parser); + } + return null; + } +} diff --git a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java new file mode 100644 index 0000000000..425a44c159 --- /dev/null +++ b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java @@ -0,0 +1,227 @@ +package org.commcare.xml.bulk; + +import org.commcare.cases.model.Case; +import org.commcare.cases.model.CaseIndex; +import org.commcare.modern.engine.cases.CaseIndexTable; +import org.javarosa.core.model.instance.TreeElement; +import org.javarosa.core.model.utils.DateUtils; +import org.javarosa.core.services.storage.IStorageUtilityIndexed; +import org.javarosa.core.util.externalizable.SerializationLimitationException; +import org.javarosa.xml.util.ActionableInvalidStructureException; +import org.javarosa.xml.util.InvalidStructureException; +import org.kxml2.io.KXmlParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * The BulkCaseInstanceXmlParser Parser is responsible for parsing case instance xml structure + * as specified in https://github.com/dimagi/commcare-core/wiki/casedb#casedb-instance-structure + */ + +// todo this and other case parsers duplicates a bunch of logic today that can be unified +public class BulkCaseInstanceXmlParser extends BulkElementParser { + + private static final String CASE_PROPERTY_CASE_ID = "case_id"; + private static final String CASE_PROPERTY_CASE_TYPE = "case_type"; + private static final String CASE_PROPERTY_OWNER_ID = "owner_id"; + private static final String CASE_PROPERTY_STATUS = "status"; + private static final String CASE_PROPERTY_CASE_NAME = "case_name"; + private static final String CASE_PROPERTY_LAST_MODIFIED = "last_modified"; + private static final String CASE_PROPERTY_DATE_OPENED = "date_opened"; + private static final String CASE_PROPERTY_EXTERNAL_ID = "external_id"; + private static final String CASE_PROPERTY_CATEGORY = "category"; + private static final String CASE_PROPERTY_STATE = "state"; + private static final String CASE_PROPERTY_INDEX = "index"; + + private final CaseIndexTable mCaseIndexTable; + private final IStorageUtilityIndexed storage; + + public BulkCaseInstanceXmlParser(KXmlParser parser, IStorageUtilityIndexed storage, + CaseIndexTable caseIndexTable) { + super(parser); + this.mCaseIndexTable = caseIndexTable; + this.storage = storage; + } + + @Override + protected void requestModelReadsForElement(TreeElement bufferedTreeElement, Set currentBulkReadSet) { + String caseId = bufferedTreeElement.getAttributeValue(null, "case_id"); + currentBulkReadSet.add(caseId); + } + + @Override + protected void preParseValidate() throws InvalidStructureException { + checkNode("case"); + } + + @Override + protected void processBufferedElement(TreeElement bufferedTreeElement, Map currentOperatingSet, + LinkedHashMap writeLog) throws InvalidStructureException { + String caseId = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_CASE_ID); + validateMandatoryProperty(CASE_PROPERTY_CASE_ID, caseId); + + String caseType = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_CASE_TYPE); + validateMandatoryProperty(CASE_PROPERTY_CASE_TYPE, caseType); + + String ownerId = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_OWNER_ID); + validateMandatoryProperty(CASE_PROPERTY_OWNER_ID, ownerId); + + String status = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_STATUS); + validateMandatoryProperty(CASE_PROPERTY_STATUS, status); + + Case caseForBlock = currentOperatingSet.get(caseId); + if (caseForBlock == null) { + caseForBlock = buildCase(null, caseType); + caseForBlock.setCaseId(caseId); + } else { + caseForBlock.setTypeId(caseType); + } + caseForBlock.setUserId(ownerId); + caseForBlock.setClosed(status.contentEquals("closed")); + + updateCase(bufferedTreeElement, caseForBlock, caseId); + validateCase(caseForBlock); + + try { + writeLog.put(caseForBlock.getCaseId(), caseForBlock); + currentOperatingSet.put(caseForBlock.getCaseId(), caseForBlock); + } catch (SerializationLimitationException e) { + throw new InvalidStructureException("One of the property values for the case named '" + + caseForBlock.getName() + "' is too large (by " + e.percentOversized + + "%). Please show your supervisor."); + } + } + + private void validateCase(Case caseForBlock) throws InvalidStructureException { + validateMandatoryProperty(CASE_PROPERTY_LAST_MODIFIED, caseForBlock.getLastModified()); + validateMandatoryProperty(CASE_PROPERTY_CASE_NAME, caseForBlock.getName()); + } + + private static String getTrimmedElementTextOrBlank(TreeElement element) { + if (element.getValue() == null) { + return ""; + } + return element.getValue().uncast().getString().trim(); + } + + private static void validateMandatoryProperty(String key, Object value) throws InvalidStructureException { + if (value == null || value.equals("")) { + String error = String.format("The %s attribute of a wasn't set", key); + throw new InvalidStructureException(error); + } + } + + protected Case buildCase(String name, String typeId) { + return new Case(name, typeId); + } + + private void updateCase(TreeElement updateElement, + Case caseForBlock, String caseId) throws InvalidStructureException { + + for (int i = 0; i < updateElement.getNumChildren(); i++) { + TreeElement subElement = updateElement.getChildAt(i); + + String key = subElement.getName(); + String value = getTrimmedElementTextOrBlank(subElement); + + switch (key) { + case CASE_PROPERTY_CASE_NAME: + caseForBlock.setName(value); + break; + case CASE_PROPERTY_DATE_OPENED: + caseForBlock.setDateOpened(DateUtils.parseDate(value)); + break; + case CASE_PROPERTY_LAST_MODIFIED: + caseForBlock.setLastModified(DateUtils.parseDateTime(value)); + break; + case CASE_PROPERTY_EXTERNAL_ID: + caseForBlock.setExternalId(value); + break; + case CASE_PROPERTY_CATEGORY: + caseForBlock.setCategory(value); + break; + case CASE_PROPERTY_STATE: + caseForBlock.setState(value); + break; + case CASE_PROPERTY_INDEX: + indexCase(subElement, caseForBlock, caseId); + break; + default: + caseForBlock.setProperty(key, value); + break; + } + } + } + + @Override + protected void performBulkRead(Set currentBulkReadSet, Map currentOperatingSet) { + for (Case c : storage.getBulkRecordsForIndex(Case.INDEX_CASE_ID, currentBulkReadSet)) { + currentOperatingSet.put(c.getCaseId(), c); + } + } + + @Override + protected void performBulkWrite(LinkedHashMap writeLog) throws IOException { + ArrayList recordIdsToWipe = new ArrayList<>(); + for (String caseId : writeLog.keySet()) { + Case c = writeLog.get(caseId); + storage.write(c); + // Add the case's SQL record ID + recordIdsToWipe.add(c.getID()); + } + if (mCaseIndexTable != null) { + mCaseIndexTable.clearCaseIndices(recordIdsToWipe); + for (String cid : writeLog.keySet()) { + Case c = writeLog.get(cid); + mCaseIndexTable.indexCase(c); + } + } + } + + private static void indexCase(TreeElement indexElement, Case caseForBlock, String caseId) + throws InvalidStructureException { + for (int i = 0; i < indexElement.getNumChildren(); i++) { + TreeElement subElement = indexElement.getChildAt(i); + + String indexName = subElement.getName(); + String caseType = subElement.getAttributeValue(null, "case_type"); + + String relationship = subElement.getAttributeValue(null, "relationship"); + if (relationship == null) { + relationship = CaseIndex.RELATIONSHIP_CHILD; + } + + String value = getTrimmedElementTextOrBlank(subElement); + + if (value.equals(caseId)) { + throw new ActionableInvalidStructureException("case.error.self.index", new String[]{caseId}, + "Case " + caseId + " cannot index itself"); + } + + //Remove any ambiguity associated with empty values + if (value.equals("")) { + value = null; + } + + if ("".equals(relationship)) { + throw new InvalidStructureException(String.format( + "Invalid Case Transaction for Case[%s]: Attempt to add a '' relationship type to " + + "entity[%s]", + caseId, value)); + } + + + //Process blank inputs in the same manner as data fields (IE: Remove the underlying model) + if (value == null) { + caseForBlock.removeIndex(indexName); + } else { + caseForBlock.setIndex(new CaseIndex(indexName, caseType, value, + relationship)); + } + } + } +} diff --git a/src/test/java/org/commcare/cases/util/test/BulkCaseInstanceXmlParserTests.java b/src/test/java/org/commcare/cases/util/test/BulkCaseInstanceXmlParserTests.java new file mode 100644 index 0000000000..8ee64c554c --- /dev/null +++ b/src/test/java/org/commcare/cases/util/test/BulkCaseInstanceXmlParserTests.java @@ -0,0 +1,52 @@ +package org.commcare.cases.util.test; + +import static org.commcare.test.utilities.CaseTestUtils.compareCaseDbState; + +import org.commcare.core.parse.CaseInstanceXmlTransactionParserFactory; +import org.commcare.core.parse.ParseUtils; +import org.commcare.data.xml.TransactionParserFactory; +import org.commcare.test.utilities.CaseTestUtils; +import org.commcare.util.mocks.MockDataUtils; +import org.commcare.util.mocks.MockUserDataSandbox; +import org.javarosa.core.model.condition.EvaluationContext; +import org.javarosa.xml.util.InvalidStructureException; +import org.javarosa.xml.util.UnfullfilledRequirementsException; +import org.javarosa.xpath.parser.XPathSyntaxException; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.InputStream; + +public class BulkCaseInstanceXmlParserTests { + + private MockUserDataSandbox sandbox; + + @Before + public void setUp() { + sandbox = MockDataUtils.getStaticStorage(); + } + + @Test + public void testValidCaseInstanceXml() + throws UnfullfilledRequirementsException, InvalidStructureException, XmlPullParserException, + IOException, XPathSyntaxException { + parseXml("case_instance_parse/case_instance_valid.xml"); + compareCaseDbState(sandbox, getClass().getClassLoader().getResourceAsStream("case_instance_parse/case_instance_output.xml")); + EvaluationContext ec = MockDataUtils.buildContextWithInstance(this.sandbox, "casedb", + CaseTestUtils.CASE_INSTANCE); + Assert.assertTrue(CaseTestUtils.xpathEvalAndCompare(ec, + "instance('casedb')/casedb/case[@case_id = 'f6dff792-2599-4fd3-9e86-c11ef61f0302']/case_name", "tapid papid")); + } + + private void parseXml(String resourceFilePath) + throws UnfullfilledRequirementsException, InvalidStructureException, XmlPullParserException, + IOException { + InputStream dataSteam = getClass().getClassLoader().getResourceAsStream(resourceFilePath); + TransactionParserFactory factory = new CaseInstanceXmlTransactionParserFactory(sandbox, null); + ParseUtils.parseIntoSandbox(dataSteam, factory, false, false); + dataSteam.close(); + } +} diff --git a/src/test/resources/case_instance_parse/case_instance_output.xml b/src/test/resources/case_instance_parse/case_instance_output.xml new file mode 100644 index 0000000000..b0aa1b42e4 --- /dev/null +++ b/src/test/resources/case_instance_parse/case_instance_output.xml @@ -0,0 +1 @@ +Delhi March2021-01-292021-01-2943171e8e32cd4b39b6d72b52e5bafc591.0f6dff792-2599-4fd3-9e86-c11ef61f0302tapid papid2021-01-192021-01-19420c714a23fdd2ac2f2e40f82bbe9659051.055 diff --git a/src/test/resources/case_instance_parse/case_instance_valid.xml b/src/test/resources/case_instance_parse/case_instance_valid.xml new file mode 100644 index 0000000000..6a4391609e --- /dev/null +++ b/src/test/resources/case_instance_parse/case_instance_valid.xml @@ -0,0 +1,22 @@ + + + tapid papid + 2021-01-19T07:30:43.472000Z + 2021-01-19 + 1.0 + 5 + 5 + 0 + 5 + + + + Delhi March + 2021-01-29T08:52:23.839000Z + 2021-01-29 + 1.0 + + f6dff792-2599-4fd3-9e86-c11ef61f0302 + + + From c254e3695c1b56bf38034d450d1b5b5dfa8c69f5 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Sat, 25 Mar 2023 12:21:13 -0300 Subject: [PATCH 089/402] adds delete and exists storage for case index table --- .../java/org/commcare/modern/engine/cases/CaseIndexTable.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/commcare/modern/engine/cases/CaseIndexTable.java b/src/main/java/org/commcare/modern/engine/cases/CaseIndexTable.java index 0570bf1759..69806807c9 100644 --- a/src/main/java/org/commcare/modern/engine/cases/CaseIndexTable.java +++ b/src/main/java/org/commcare/modern/engine/cases/CaseIndexTable.java @@ -23,4 +23,8 @@ public interface CaseIndexTable { void indexCase(Case c); void clearCaseIndices(Collection idsToClear); + + void delete(); + + boolean isStorageExists(); } From 84c6ffc135c77bc224d0a7f99a70851ed24cb0d6 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Sat, 25 Mar 2023 12:23:04 -0300 Subject: [PATCH 090/402] adds delete storage method --- .../core/services/storage/IStorageUtilityIndexed.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java b/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java index aeb28f645b..aa1834fce7 100644 --- a/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java +++ b/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java @@ -294,4 +294,9 @@ void bulkRead(LinkedHashSet cuedCases, HashMap recordMap) * initialise the storage, for example create the table in the DB for this storage */ void initStorage(); + + /** + * Deletes the storage + */ + void deleteStorage(); } From 047c49f76595bb4b9e56eb928b2cd50a84abaed4 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Sat, 25 Mar 2023 12:24:25 -0300 Subject: [PATCH 091/402] implements new storage methods for dummy storage --- .../storage/util/DummyIndexedStorageUtility.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java b/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java index b679e7a15d..33874b4d9f 100644 --- a/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java +++ b/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java @@ -32,7 +32,7 @@ */ public class DummyIndexedStorageUtility implements IStorageUtilityIndexed { - private final Hashtable>> meta = new Hashtable<>(); + private Hashtable>> meta = new Hashtable<>(); private final Hashtable data = new Hashtable<>(); private int curCount = 0; private final Class prototype; @@ -381,13 +381,16 @@ public Class getPrototype() { @Override public boolean isStorageExists() { - // this method is not really meaningful for dummy storage - // so default to returning true until needed otherwise - return true; + return meta != null; } @Override public void initStorage() { - // do nothing as storage is initialized by default + meta = new Hashtable<>(); + } + + @Override + public void deleteStorage() { + meta = null; } } From ce98765a0a916902c21838b1d76b8e6201614c9d Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Sat, 25 Mar 2023 23:04:19 -0300 Subject: [PATCH 092/402] adds file delete methods to utils --- .../java/org/commcare/util/FileUtils.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/main/java/org/commcare/util/FileUtils.java b/src/main/java/org/commcare/util/FileUtils.java index adb9232d86..be4b0cd869 100644 --- a/src/main/java/org/commcare/util/FileUtils.java +++ b/src/main/java/org/commcare/util/FileUtils.java @@ -11,6 +11,11 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import javax.annotation.Nullable; @@ -68,4 +73,40 @@ public static String getExtension(String file) { } return ""; } + + /** + * Delete files inside the given folder if they were las accessed before the given cutOff time + * + * @param folder folder for which we want to delete the files + * @param cutOff cutOff time before which we want to delete the files + * @return number of files deleted + * @throws IOException + */ + public static int deleteFiles(File folder, Instant cutOff) throws IOException { + File[] files = folder.listFiles(); + int count = 0; + for (File file : files) { + if (file.isDirectory()) { + deleteFiles(file, cutOff); + } else { + deleteFile(file, cutOff); + count++; + } + } + return count; + } + + /** + * Delete given file if it was las accessed before the given cutOff time + * @param file file to be deleted + * @param cutOff cutOff time before which we want to delete the file + * @throws IOException + */ + public static void deleteFile(File file, Instant cutOff) throws IOException { + BasicFileAttributes attr = Files.readAttributes(Paths.get(file.getPath()), BasicFileAttributes.class); + FileTime lastAccessTime = attr.lastAccessTime(); + if (lastAccessTime.toInstant().isBefore(cutOff)) { + file.delete(); + } + } } From a628f5ac4c39ff6e8013f61d3ea346d1f6f772dd Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 11 Apr 2023 09:05:12 -0300 Subject: [PATCH 093/402] output case_id in case validation errors --- .../xml/bulk/BulkCaseInstanceXmlParser.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java index 425a44c159..2d48a3cb79 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java @@ -62,16 +62,16 @@ protected void preParseValidate() throws InvalidStructureException { protected void processBufferedElement(TreeElement bufferedTreeElement, Map currentOperatingSet, LinkedHashMap writeLog) throws InvalidStructureException { String caseId = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_CASE_ID); - validateMandatoryProperty(CASE_PROPERTY_CASE_ID, caseId); + validateMandatoryProperty(CASE_PROPERTY_CASE_ID, caseId, ""); String caseType = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_CASE_TYPE); - validateMandatoryProperty(CASE_PROPERTY_CASE_TYPE, caseType); + validateMandatoryProperty(CASE_PROPERTY_CASE_TYPE, caseType, caseId); String ownerId = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_OWNER_ID); - validateMandatoryProperty(CASE_PROPERTY_OWNER_ID, ownerId); + validateMandatoryProperty(CASE_PROPERTY_OWNER_ID, ownerId, caseId); String status = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_STATUS); - validateMandatoryProperty(CASE_PROPERTY_STATUS, status); + validateMandatoryProperty(CASE_PROPERTY_STATUS, status, caseId); Case caseForBlock = currentOperatingSet.get(caseId); if (caseForBlock == null) { @@ -97,8 +97,8 @@ protected void processBufferedElement(TreeElement bufferedTreeElement, Map wasn't set", key); + String error = String.format("The %s attribute of a %s wasn't set", key, caseId); throw new InvalidStructureException(error); } } From cd44c29a532c9413cd19ec1e4352e7e0cd9ec00b Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 12 Apr 2023 13:17:08 -0300 Subject: [PATCH 094/402] corrects spelling --- src/main/java/org/commcare/util/FileUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/commcare/util/FileUtils.java b/src/main/java/org/commcare/util/FileUtils.java index be4b0cd869..ae5f1f944e 100644 --- a/src/main/java/org/commcare/util/FileUtils.java +++ b/src/main/java/org/commcare/util/FileUtils.java @@ -75,7 +75,7 @@ public static String getExtension(String file) { } /** - * Delete files inside the given folder if they were las accessed before the given cutOff time + * Delete files inside the given folder if they were last accessed before the given cutOff time * * @param folder folder for which we want to delete the files * @param cutOff cutOff time before which we want to delete the files @@ -97,7 +97,7 @@ public static int deleteFiles(File folder, Instant cutOff) throws IOException { } /** - * Delete given file if it was las accessed before the given cutOff time + * Delete given file if it was last accessed before the given cutOff time * @param file file to be deleted * @param cutOff cutOff time before which we want to delete the file * @throws IOException From 5e0311baef90dc2fa141e9d0aeba59d4717b1641 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 12 Apr 2023 13:19:58 -0300 Subject: [PATCH 095/402] move relationship validation in else if --- .../xml/bulk/BulkCaseInstanceXmlParser.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java index 2d48a3cb79..2edcf28f7e 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java @@ -190,13 +190,17 @@ private static void indexCase(TreeElement indexElement, Case caseForBlock, Strin String indexName = subElement.getName(); String caseType = subElement.getAttributeValue(null, "case_type"); + String value = getTrimmedElementTextOrBlank(subElement); String relationship = subElement.getAttributeValue(null, "relationship"); if (relationship == null) { relationship = CaseIndex.RELATIONSHIP_CHILD; + } else if ("".equals(relationship)) { + throw new InvalidStructureException(String.format( + "Invalid Case Transaction for Case[%s]: Attempt to add a '' relationship type to " + + "entity[%s]", + caseId, value)); } - String value = getTrimmedElementTextOrBlank(subElement); - if (value.equals(caseId)) { throw new ActionableInvalidStructureException("case.error.self.index", new String[]{caseId}, "Case " + caseId + " cannot index itself"); @@ -207,14 +211,6 @@ private static void indexCase(TreeElement indexElement, Case caseForBlock, Strin value = null; } - if ("".equals(relationship)) { - throw new InvalidStructureException(String.format( - "Invalid Case Transaction for Case[%s]: Attempt to add a '' relationship type to " - + "entity[%s]", - caseId, value)); - } - - //Process blank inputs in the same manner as data fields (IE: Remove the underlying model) if (value == null) { caseForBlock.removeIndex(indexName); From 1fdd4536afe2cfcbd11b6fdfe4f4f02208b7aec8 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 12 Apr 2023 13:26:55 -0300 Subject: [PATCH 096/402] stickler --- .../core/parse/CaseInstanceXmlTransactionParserFactory.java | 6 ++++++ src/main/java/org/commcare/util/FileUtils.java | 5 +++-- .../org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java | 6 ++++-- .../core/services/storage/IStorageUtilityIndexed.java | 1 + .../java/org/commcare/cases/test/CaseParseAndReadTest.java | 3 ++- .../cases/util/test/BulkCaseInstanceXmlParserTests.java | 6 ++++-- 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/commcare/core/parse/CaseInstanceXmlTransactionParserFactory.java b/src/main/java/org/commcare/core/parse/CaseInstanceXmlTransactionParserFactory.java index bfb5cc1856..87536b03c8 100644 --- a/src/main/java/org/commcare/core/parse/CaseInstanceXmlTransactionParserFactory.java +++ b/src/main/java/org/commcare/core/parse/CaseInstanceXmlTransactionParserFactory.java @@ -7,6 +7,12 @@ import org.commcare.xml.bulk.BulkCaseInstanceXmlParser; import org.kxml2.io.KXmlParser; +/** + * Transaction factory for parsing the case instance xml as defined in + * https://github.com/dimagi/commcare-core/wiki/casedb + * + * It's primary usage is to parse case instance xml from case query API + */ public class CaseInstanceXmlTransactionParserFactory implements TransactionParserFactory { private final UserSandbox sandbox; diff --git a/src/main/java/org/commcare/util/FileUtils.java b/src/main/java/org/commcare/util/FileUtils.java index ae5f1f944e..e28c1dba08 100644 --- a/src/main/java/org/commcare/util/FileUtils.java +++ b/src/main/java/org/commcare/util/FileUtils.java @@ -87,7 +87,7 @@ public static int deleteFiles(File folder, Instant cutOff) throws IOException { int count = 0; for (File file : files) { if (file.isDirectory()) { - deleteFiles(file, cutOff); + deleteFiles(file, cutOff); } else { deleteFile(file, cutOff); count++; @@ -98,7 +98,8 @@ public static int deleteFiles(File folder, Instant cutOff) throws IOException { /** * Delete given file if it was last accessed before the given cutOff time - * @param file file to be deleted + * + * @param file file to be deleted * @param cutOff cutOff time before which we want to delete the file * @throws IOException */ diff --git a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java index 2edcf28f7e..c4b881876a 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java @@ -97,7 +97,8 @@ protected void processBufferedElement(TreeElement bufferedTreeElement, Map %s wasn't set", key, caseId); throw new InvalidStructureException(error); diff --git a/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java b/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java index aa1834fce7..9fc30ba7cb 100644 --- a/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java +++ b/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java @@ -286,6 +286,7 @@ void bulkRead(LinkedHashSet cuedCases, HashMap recordMap) /** * if the storage exists or not + * * @return a boolean indicating if the storage exists */ boolean isStorageExists(); diff --git a/src/test/java/org/commcare/cases/test/CaseParseAndReadTest.java b/src/test/java/org/commcare/cases/test/CaseParseAndReadTest.java index e6cfdcf49f..4813ef526a 100644 --- a/src/test/java/org/commcare/cases/test/CaseParseAndReadTest.java +++ b/src/test/java/org/commcare/cases/test/CaseParseAndReadTest.java @@ -70,8 +70,9 @@ public void testDoubleCreateCaseWithUpdate() throws Exception { Assert.assertTrue(CaseTestUtils.xpathEvalAndCompare(ec, "instance('casedb')/casedb/case[@case_id = 'case_one']/case_property1", "one")); Assert.assertTrue(CaseTestUtils.xpathEvalAndCompare(ec, "instance('casedb')/casedb/case[@case_id = 'case_one']/case_property2", "property_two")); } + private void parseAndCompareCaseDbState(String inputTransactions, - String caseDbState) throws Exception { + String caseDbState) throws Exception { config.parseIntoSandbox(this.getClass().getResourceAsStream(inputTransactions), sandbox, false); compareCaseDbState(sandbox, getClass().getResourceAsStream(caseDbState)); } diff --git a/src/test/java/org/commcare/cases/util/test/BulkCaseInstanceXmlParserTests.java b/src/test/java/org/commcare/cases/util/test/BulkCaseInstanceXmlParserTests.java index 8ee64c554c..28f9729831 100644 --- a/src/test/java/org/commcare/cases/util/test/BulkCaseInstanceXmlParserTests.java +++ b/src/test/java/org/commcare/cases/util/test/BulkCaseInstanceXmlParserTests.java @@ -34,11 +34,13 @@ public void testValidCaseInstanceXml() throws UnfullfilledRequirementsException, InvalidStructureException, XmlPullParserException, IOException, XPathSyntaxException { parseXml("case_instance_parse/case_instance_valid.xml"); - compareCaseDbState(sandbox, getClass().getClassLoader().getResourceAsStream("case_instance_parse/case_instance_output.xml")); + compareCaseDbState(sandbox, + getClass().getClassLoader().getResourceAsStream("case_instance_parse/case_instance_output.xml")); EvaluationContext ec = MockDataUtils.buildContextWithInstance(this.sandbox, "casedb", CaseTestUtils.CASE_INSTANCE); Assert.assertTrue(CaseTestUtils.xpathEvalAndCompare(ec, - "instance('casedb')/casedb/case[@case_id = 'f6dff792-2599-4fd3-9e86-c11ef61f0302']/case_name", "tapid papid")); + "instance('casedb')/casedb/case[@case_id = 'f6dff792-2599-4fd3-9e86-c11ef61f0302']/case_name", + "tapid papid")); } private void parseXml(String resourceFilePath) From 3eb5f7f69f070ed2e5a7833891b1d80001575be6 Mon Sep 17 00:00:00 2001 From: Jonathan Tang <88759246+Jtang-1@users.noreply.github.com> Date: Fri, 21 Apr 2023 09:08:41 -0700 Subject: [PATCH 097/402] Highlight duplication steps within PR --- .github/PULL_REQUEST_TEMPLATE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a55febf349..f484cc8bf3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -54,3 +54,6 @@ Otherwise replace it with detailed instructions or reasons a rollback is impossi ### Review - [ ] The set of people pinged as reviewers is appropriate for the level of risk of the change. + +### Duplicate PR +Automatically duplicate this PR as defined in [contributing.md](https://github.com/dimagi/commcare-core/blob/de31d3b5729eefb994371ab55cdf1f3935679e1c/.github/contributing.md). From 00ef3dfd89f41cf7440c9ba101cc22b73b92053e Mon Sep 17 00:00:00 2001 From: Jonathan Tang <88759246+Jtang-1@users.noreply.github.com> Date: Fri, 21 Apr 2023 10:36:01 -0700 Subject: [PATCH 098/402] Corrects link to point to latest --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f484cc8bf3..4bf89fe775 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -56,4 +56,4 @@ Otherwise replace it with detailed instructions or reasons a rollback is impossi - [ ] The set of people pinged as reviewers is appropriate for the level of risk of the change. ### Duplicate PR -Automatically duplicate this PR as defined in [contributing.md](https://github.com/dimagi/commcare-core/blob/de31d3b5729eefb994371ab55cdf1f3935679e1c/.github/contributing.md). +Automatically duplicate this PR as defined in [contributing.md](https://github.com/dimagi/commcare-core/blob/master/.github/contributing.md). From 518c046835dccd5a4324634c9ecd4d878ac35823 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 11 Apr 2023 22:12:09 -0300 Subject: [PATCH 099/402] move constants to a util class --- .../java/org/commcare/xml/CaseXmlParser.java | 6 ----- .../org/commcare/xml/CaseXmlParserUtil.java | 21 ++++++++++++++++ .../xml/bulk/BulkCaseInstanceXmlParser.java | 24 +++++++++---------- 3 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/commcare/xml/CaseXmlParserUtil.java diff --git a/src/main/java/org/commcare/xml/CaseXmlParser.java b/src/main/java/org/commcare/xml/CaseXmlParser.java index 21c189890d..f92f2fbd42 100644 --- a/src/main/java/org/commcare/xml/CaseXmlParser.java +++ b/src/main/java/org/commcare/xml/CaseXmlParser.java @@ -31,12 +31,6 @@ */ public class CaseXmlParser extends TransactionParser { - public static final String ATTACHMENT_FROM_LOCAL = "local"; - public static final String ATTACHMENT_FROM_REMOTE = "remote"; - public static final String ATTACHMENT_FROM_INLINE = "inline"; - - public static final String CASE_XML_NAMESPACE = "http://commcarehq.org/case/transaction/v2"; - private final IStorageUtilityIndexed storage; private final boolean acceptCreateOverwrites; diff --git a/src/main/java/org/commcare/xml/CaseXmlParserUtil.java b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java new file mode 100644 index 0000000000..c0e40a0e13 --- /dev/null +++ b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java @@ -0,0 +1,21 @@ +package org.commcare.xml; + +public class CaseXmlParserUtil { + + public static final String CASE_PROPERTY_CASE_ID = "case_id"; + public static final String CASE_PROPERTY_CASE_TYPE = "case_type"; + public static final String CASE_PROPERTY_OWNER_ID = "owner_id"; + public static final String CASE_PROPERTY_STATUS = "status"; + public static final String CASE_PROPERTY_CASE_NAME = "case_name"; + public static final String CASE_PROPERTY_LAST_MODIFIED = "last_modified"; + public static final String CASE_PROPERTY_DATE_OPENED = "date_opened"; + public static final String CASE_PROPERTY_EXTERNAL_ID = "external_id"; + public static final String CASE_PROPERTY_CATEGORY = "category"; + public static final String CASE_PROPERTY_STATE = "state"; + public static final String CASE_PROPERTY_INDEX = "index"; + public static final String ATTACHMENT_FROM_LOCAL = "local"; + public static final String ATTACHMENT_FROM_REMOTE = "remote"; + public static final String ATTACHMENT_FROM_INLINE = "inline"; + public static final String CASE_XML_NAMESPACE = "http://commcarehq.org/case/transaction/v2"; + +} diff --git a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java index c4b881876a..e411e464f6 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java @@ -1,5 +1,17 @@ package org.commcare.xml.bulk; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_ID; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_NAME; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_TYPE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CATEGORY; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_DATE_OPENED; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_EXTERNAL_ID; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_INDEX; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_LAST_MODIFIED; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_OWNER_ID; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATUS; + import org.commcare.cases.model.Case; import org.commcare.cases.model.CaseIndex; import org.commcare.modern.engine.cases.CaseIndexTable; @@ -25,18 +37,6 @@ // todo this and other case parsers duplicates a bunch of logic today that can be unified public class BulkCaseInstanceXmlParser extends BulkElementParser { - private static final String CASE_PROPERTY_CASE_ID = "case_id"; - private static final String CASE_PROPERTY_CASE_TYPE = "case_type"; - private static final String CASE_PROPERTY_OWNER_ID = "owner_id"; - private static final String CASE_PROPERTY_STATUS = "status"; - private static final String CASE_PROPERTY_CASE_NAME = "case_name"; - private static final String CASE_PROPERTY_LAST_MODIFIED = "last_modified"; - private static final String CASE_PROPERTY_DATE_OPENED = "date_opened"; - private static final String CASE_PROPERTY_EXTERNAL_ID = "external_id"; - private static final String CASE_PROPERTY_CATEGORY = "category"; - private static final String CASE_PROPERTY_STATE = "state"; - private static final String CASE_PROPERTY_INDEX = "index"; - private final CaseIndexTable mCaseIndexTable; private final IStorageUtilityIndexed storage; From e7289a818510950278959ad93cc4329619b6ece9 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 11 Apr 2023 22:28:32 -0300 Subject: [PATCH 100/402] move validate method to util with a more specific exception --- .../org/commcare/xml/CaseXmlParserUtil.java | 16 ++++++++++++- .../xml/bulk/BulkCaseInstanceXmlParser.java | 24 +++++++------------ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/commcare/xml/CaseXmlParserUtil.java b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java index c0e40a0e13..c0c6348531 100644 --- a/src/main/java/org/commcare/xml/CaseXmlParserUtil.java +++ b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java @@ -1,13 +1,18 @@ package org.commcare.xml; -public class CaseXmlParserUtil { +import org.javarosa.xml.util.InvalidStructureException; +import org.kxml2.io.KXmlParser; +public class CaseXmlParserUtil { + public static final String CASE_NODE_NAME = "case"; public static final String CASE_PROPERTY_CASE_ID = "case_id"; public static final String CASE_PROPERTY_CASE_TYPE = "case_type"; public static final String CASE_PROPERTY_OWNER_ID = "owner_id"; + public static final String CASE_PROPERTY_USER_ID = "user_id"; public static final String CASE_PROPERTY_STATUS = "status"; public static final String CASE_PROPERTY_CASE_NAME = "case_name"; public static final String CASE_PROPERTY_LAST_MODIFIED = "last_modified"; + public static final String CASE_PROPERTY_DATE_MODIFIED = "date_modified"; public static final String CASE_PROPERTY_DATE_OPENED = "date_opened"; public static final String CASE_PROPERTY_EXTERNAL_ID = "external_id"; public static final String CASE_PROPERTY_CATEGORY = "category"; @@ -18,4 +23,13 @@ public class CaseXmlParserUtil { public static final String ATTACHMENT_FROM_INLINE = "inline"; public static final String CASE_XML_NAMESPACE = "http://commcarehq.org/case/transaction/v2"; + + public static void validateMandatoryProperty(String key, Object value, String caseId, KXmlParser parser) throws + InvalidStructureException { + if (value == null || value.equals("")) { + String error = String.format("The %s attribute of a %s wasn't set", key, caseId); + throw InvalidStructureException.readableInvalidStructureException(error, parser); + } + } + } diff --git a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java index e411e464f6..6ae91d3037 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java @@ -11,6 +11,7 @@ import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_OWNER_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATE; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATUS; +import static org.commcare.xml.CaseXmlParserUtil.validateMandatoryProperty; import org.commcare.cases.model.Case; import org.commcare.cases.model.CaseIndex; @@ -62,16 +63,16 @@ protected void preParseValidate() throws InvalidStructureException { protected void processBufferedElement(TreeElement bufferedTreeElement, Map currentOperatingSet, LinkedHashMap writeLog) throws InvalidStructureException { String caseId = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_CASE_ID); - validateMandatoryProperty(CASE_PROPERTY_CASE_ID, caseId, ""); + validateMandatoryProperty(CASE_PROPERTY_CASE_ID, caseId, "", parser); String caseType = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_CASE_TYPE); - validateMandatoryProperty(CASE_PROPERTY_CASE_TYPE, caseType, caseId); + validateMandatoryProperty(CASE_PROPERTY_CASE_TYPE, caseType, caseId, parser); String ownerId = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_OWNER_ID); - validateMandatoryProperty(CASE_PROPERTY_OWNER_ID, ownerId, caseId); + validateMandatoryProperty(CASE_PROPERTY_OWNER_ID, ownerId, caseId, parser); String status = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_STATUS); - validateMandatoryProperty(CASE_PROPERTY_STATUS, status, caseId); + validateMandatoryProperty(CASE_PROPERTY_STATUS, status, caseId, parser); Case caseForBlock = currentOperatingSet.get(caseId); if (caseForBlock == null) { @@ -97,9 +98,10 @@ protected void processBufferedElement(TreeElement bufferedTreeElement, Map %s wasn't set", key, caseId); - throw new InvalidStructureException(error); - } - } - protected Case buildCase(String name, String typeId) { return new Case(name, typeId); } From 799d116325c48071854262f71fc2cee9d0aee19d Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 11 Apr 2023 22:34:50 -0300 Subject: [PATCH 101/402] utilise util class constants and methods --- .../java/org/commcare/xml/CaseXmlParser.java | 53 +++++++++++-------- .../xml/bulk/BulkProcessingCaseXmlParser.java | 48 ++++++++++------- 2 files changed, 60 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/commcare/xml/CaseXmlParser.java b/src/main/java/org/commcare/xml/CaseXmlParser.java index f92f2fbd42..7a1fc9398b 100644 --- a/src/main/java/org/commcare/xml/CaseXmlParser.java +++ b/src/main/java/org/commcare/xml/CaseXmlParser.java @@ -1,5 +1,19 @@ package org.commcare.xml; +import static org.commcare.xml.CaseXmlParserUtil.CASE_NODE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_NODE_NAME; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_ID; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_NAME; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_TYPE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CATEGORY; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_DATE_MODIFIED; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_DATE_OPENED; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_EXTERNAL_ID; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_OWNER_ID; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_USER_ID; +import static org.commcare.xml.CaseXmlParserUtil.validateMandatoryProperty; + import org.commcare.cases.model.Case; import org.commcare.cases.model.CaseIndex; import org.commcare.data.xml.TransactionParser; @@ -55,25 +69,22 @@ public CaseXmlParser(KXmlParser parser, boolean acceptCreateOverwrites, @Override public Case parse() throws InvalidStructureException, IOException, XmlPullParserException { - checkNode("case"); + checkNode(CASE_NODE_NAME); - String caseId = parser.getAttributeValue(null, "case_id"); - if (caseId == null || caseId.equals("")) { - throw InvalidStructureException.readableInvalidStructureException("The case_id attribute of a wasn't set", parser); - } + String caseId = parser.getAttributeValue(null, CASE_PROPERTY_CASE_ID); + validateMandatoryProperty(CASE_PROPERTY_CASE_ID, caseId, "", parser); + + String dateModified = parser.getAttributeValue(null, CASE_PROPERTY_DATE_MODIFIED); + validateMandatoryProperty(CASE_PROPERTY_DATE_MODIFIED, dateModified, caseId, parser); - String dateModified = parser.getAttributeValue(null, "date_modified"); - if (dateModified == null) { - throw InvalidStructureException.readableInvalidStructureException("The date_modified attribute of a wasn't set", parser); - } Date modified = DateUtils.parseDateTime(dateModified); - String userId = parser.getAttributeValue(null, "user_id"); + String userId = parser.getAttributeValue(null, CASE_PROPERTY_USER_ID); Case caseForBlock = null; boolean isCreateOrUpdate = false; - while (nextTagInBlock("case")) { + while (nextTagInBlock(CASE_NODE_NAME)) { String action = parser.getName().toLowerCase(); switch (action) { case "create": @@ -126,13 +137,13 @@ private Case createCase(String caseId, Date modified, String userId) throws Inva while (nextTagInBlock("create")) { String tag = parser.getName(); switch (tag) { - case "case_type": + case CASE_PROPERTY_CASE_TYPE: data[0] = parser.nextText().trim(); break; - case "owner_id": + case CASE_PROPERTY_OWNER_ID: data[1] = parser.nextText().trim(); break; - case "case_name": + case CASE_PROPERTY_CASE_NAME: data[2] = parser.nextText().trim(); break; default: @@ -181,16 +192,16 @@ private void updateCase(Case caseForBlock, String caseId) throws InvalidStructur String value = parser.nextText().trim(); switch (key) { - case "case_type": + case CASE_PROPERTY_CASE_TYPE: caseForBlock.setTypeId(value); break; - case "case_name": + case CASE_PROPERTY_CASE_NAME: caseForBlock.setName(value); break; - case "date_opened": + case CASE_PROPERTY_DATE_OPENED: caseForBlock.setDateOpened(DateUtils.parseDate(value)); break; - case "owner_id": + case CASE_PROPERTY_OWNER_ID: String oldUserId = caseForBlock.getUserId(); if (!oldUserId.equals(value)) { @@ -198,13 +209,13 @@ private void updateCase(Case caseForBlock, String caseId) throws InvalidStructur } caseForBlock.setUserId(value); break; - case "external_id": + case CASE_PROPERTY_EXTERNAL_ID: caseForBlock.setExternalId(value); break; - case "category": + case CASE_PROPERTY_CATEGORY: caseForBlock.setCategory(value); break; - case "state": + case CASE_PROPERTY_STATE: caseForBlock.setState(value); break; default: diff --git a/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java index 149c4bae02..22a233c0f6 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java @@ -1,5 +1,17 @@ package org.commcare.xml.bulk; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_ID; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_NAME; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_TYPE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CATEGORY; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_DATE_MODIFIED; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_DATE_OPENED; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_EXTERNAL_ID; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_OWNER_ID; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_USER_ID; +import static org.commcare.xml.CaseXmlParserUtil.validateMandatoryProperty; + import org.commcare.cases.model.Case; import org.commcare.cases.model.CaseIndex; import org.javarosa.core.model.instance.TreeElement; @@ -43,18 +55,14 @@ protected void preParseValidate() throws InvalidStructureException { @Override protected void processBufferedElement(TreeElement bufferedTreeElement, Map currentOperatingSet, LinkedHashMap writeLog) throws InvalidStructureException { - String caseId = bufferedTreeElement.getAttributeValue(null, "case_id"); - if (caseId == null || caseId.equals("")) { - throw new InvalidStructureException("The case_id attribute of a wasn't set"); - } + String caseId = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_CASE_ID); + validateMandatoryProperty(CASE_PROPERTY_CASE_ID, caseId, "", parser); - String dateModified = bufferedTreeElement.getAttributeValue(null, "date_modified"); - if (dateModified == null) { - throw new InvalidStructureException("The date_modified attribute of a wasn't set"); - } + String dateModified = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_DATE_MODIFIED); + validateMandatoryProperty(CASE_PROPERTY_DATE_MODIFIED, dateModified, caseId, parser); Date modified = DateUtils.parseDateTime(dateModified); - String userId = parser.getAttributeValue(null, "user_id"); + String userId = parser.getAttributeValue(null, CASE_PROPERTY_USER_ID); Case caseForBlock = null; boolean isCreateOrUpdate = false; @@ -119,19 +127,19 @@ private Case createCase(TreeElement createElement, Map currentOper String caseId, Date modified, String userId) throws InvalidStructureException { String[] data = new String[3]; - Case caseForBlock = null; + Case caseForBlock; for (int i = 0; i < createElement.getNumChildren(); i++) { TreeElement subElement = createElement.getChildAt(i); String tag = subElement.getName(); switch (tag) { - case "case_type": + case CASE_PROPERTY_CASE_TYPE: data[0] = getTrimmedElementTextOrBlank(subElement); break; - case "owner_id": + case CASE_PROPERTY_OWNER_ID: data[1] = getTrimmedElementTextOrBlank(subElement); break; - case "case_name": + case CASE_PROPERTY_CASE_NAME: data[2] = getTrimmedElementTextOrBlank(subElement); break; default: @@ -193,16 +201,16 @@ private void updateCase(TreeElement updateElement, String value = getTrimmedElementTextOrBlank(subElement); switch (key) { - case "case_type": + case CASE_PROPERTY_CASE_TYPE: caseForBlock.setTypeId(value); break; - case "case_name": + case CASE_PROPERTY_CASE_NAME: caseForBlock.setName(value); break; - case "date_opened": + case CASE_PROPERTY_DATE_OPENED: caseForBlock.setDateOpened(DateUtils.parseDate(value)); break; - case "owner_id": + case CASE_PROPERTY_OWNER_ID: String oldUserId = caseForBlock.getUserId(); if (!oldUserId.equals(value)) { @@ -210,13 +218,13 @@ private void updateCase(TreeElement updateElement, } caseForBlock.setUserId(value); break; - case "external_id": + case CASE_PROPERTY_EXTERNAL_ID: caseForBlock.setExternalId(value); break; - case "category": + case CASE_PROPERTY_CATEGORY: caseForBlock.setCategory(value); break; - case "state": + case CASE_PROPERTY_STATE: caseForBlock.setState(value); break; default: From c1d01437ad1968bad9658ebfc1a2721bca4ecb6e Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 11 Apr 2023 22:43:08 -0300 Subject: [PATCH 102/402] move checkForMaxLength to util class --- .../java/org/commcare/xml/CaseXmlParser.java | 20 +----------------- .../org/commcare/xml/CaseXmlParserUtil.java | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/commcare/xml/CaseXmlParser.java b/src/main/java/org/commcare/xml/CaseXmlParser.java index 7a1fc9398b..edb75a11be 100644 --- a/src/main/java/org/commcare/xml/CaseXmlParser.java +++ b/src/main/java/org/commcare/xml/CaseXmlParser.java @@ -12,6 +12,7 @@ import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_OWNER_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATE; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_USER_ID; +import static org.commcare.xml.CaseXmlParserUtil.checkForMaxLength; import static org.commcare.xml.CaseXmlParserUtil.validateMandatoryProperty; import org.commcare.cases.model.Case; @@ -226,25 +227,6 @@ private void updateCase(Case caseForBlock, String caseId) throws InvalidStructur checkForMaxLength(caseForBlock); } - private void checkForMaxLength(Case caseForBlock) throws InvalidStructureException { - if (getStringLength(caseForBlock.getTypeId()) > 255) { - throw new InvalidCasePropertyLengthException("case_type"); - } else if (getStringLength(caseForBlock.getUserId()) > 255) { - throw new InvalidCasePropertyLengthException("owner_id"); - } else if (getStringLength(caseForBlock.getName()) > 255) { - throw new InvalidCasePropertyLengthException("case_name"); - } else if (getStringLength(caseForBlock.getExternalId()) > 255) { - throw new InvalidCasePropertyLengthException("external_id"); - } - } - - /** - * Returns the length of string if it's not null, otherwise 0. - */ - private int getStringLength(String input) { - return input != null ? input.length() : 0; - } - private Case loadCase(Case caseForBlock, String caseId, boolean errorIfMissing) throws InvalidStructureException { if (caseForBlock == null) { caseForBlock = retrieve(caseId); diff --git a/src/main/java/org/commcare/xml/CaseXmlParserUtil.java b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java index c0c6348531..18a7b32d4d 100644 --- a/src/main/java/org/commcare/xml/CaseXmlParserUtil.java +++ b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java @@ -1,5 +1,7 @@ package org.commcare.xml; +import org.commcare.cases.model.Case; +import org.javarosa.xml.util.InvalidCasePropertyLengthException; import org.javarosa.xml.util.InvalidStructureException; import org.kxml2.io.KXmlParser; @@ -32,4 +34,23 @@ public static void validateMandatoryProperty(String key, Object value, String ca } } + protected static void checkForMaxLength(Case caseForBlock) throws InvalidStructureException { + if (getStringLength(caseForBlock.getTypeId()) > 255) { + throw new InvalidCasePropertyLengthException(CASE_PROPERTY_CASE_TYPE); + } else if (getStringLength(caseForBlock.getUserId()) > 255) { + throw new InvalidCasePropertyLengthException(CASE_PROPERTY_OWNER_ID); + } else if (getStringLength(caseForBlock.getName()) > 255) { + throw new InvalidCasePropertyLengthException(CASE_PROPERTY_CASE_NAME); + } else if (getStringLength(caseForBlock.getExternalId()) > 255) { + throw new InvalidCasePropertyLengthException(CASE_PROPERTY_EXTERNAL_ID); + } + } + + /** + * Returns the length of string if it's not null, otherwise 0. + */ + private static int getStringLength(String input) { + return input != null ? input.length() : 0; + } + } From 8e81791ec584539a1514345d79bb349a1d959eea Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 19 Apr 2023 17:34:48 -0300 Subject: [PATCH 103/402] more constants and remove unused constants --- .../java/org/commcare/xml/CaseXmlParser.java | 44 +++++++++++-------- .../org/commcare/xml/CaseXmlParserUtil.java | 16 ++++--- .../xml/bulk/BulkCaseInstanceXmlParser.java | 11 +++-- .../xml/bulk/BulkProcessingCaseXmlParser.java | 33 +++++++++----- 4 files changed, 66 insertions(+), 38 deletions(-) diff --git a/src/main/java/org/commcare/xml/CaseXmlParser.java b/src/main/java/org/commcare/xml/CaseXmlParser.java index edb75a11be..2734c24641 100644 --- a/src/main/java/org/commcare/xml/CaseXmlParser.java +++ b/src/main/java/org/commcare/xml/CaseXmlParser.java @@ -1,7 +1,13 @@ package org.commcare.xml; +import static org.commcare.xml.CaseXmlParserUtil.CASE_ATTACHMENT_NODE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_CLOSE_NODE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_CREATE_NODE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_INDEX_NODE; import static org.commcare.xml.CaseXmlParserUtil.CASE_NODE; -import static org.commcare.xml.CaseXmlParserUtil.CASE_NODE_NAME; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_ATTACHMENT_FROM; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_ATTACHMENT_NAME; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_ATTACHMENT_SRC; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_NAME; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_TYPE; @@ -9,9 +15,12 @@ import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_DATE_MODIFIED; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_DATE_OPENED; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_EXTERNAL_ID; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_INDEX_CASE_TYPE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_INDEX_RELATIONSHIP; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_OWNER_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATE; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_USER_ID; +import static org.commcare.xml.CaseXmlParserUtil.CASE_UPDATE_NODE; import static org.commcare.xml.CaseXmlParserUtil.checkForMaxLength; import static org.commcare.xml.CaseXmlParserUtil.validateMandatoryProperty; @@ -22,7 +31,6 @@ import org.javarosa.core.services.storage.IStorageUtilityIndexed; import org.javarosa.core.util.externalizable.SerializationLimitationException; import org.javarosa.xml.util.ActionableInvalidStructureException; -import org.javarosa.xml.util.InvalidCasePropertyLengthException; import org.javarosa.xml.util.InvalidStructureException; import org.kxml2.io.KXmlParser; import org.xmlpull.v1.XmlPullParserException; @@ -70,7 +78,7 @@ public CaseXmlParser(KXmlParser parser, boolean acceptCreateOverwrites, @Override public Case parse() throws InvalidStructureException, IOException, XmlPullParserException { - checkNode(CASE_NODE_NAME); + checkNode(CASE_NODE); String caseId = parser.getAttributeValue(null, CASE_PROPERTY_CASE_ID); validateMandatoryProperty(CASE_PROPERTY_CASE_ID, caseId, "", parser); @@ -85,27 +93,27 @@ public Case parse() throws InvalidStructureException, IOException, XmlPullParser Case caseForBlock = null; boolean isCreateOrUpdate = false; - while (nextTagInBlock(CASE_NODE_NAME)) { + while (nextTagInBlock(CASE_NODE)) { String action = parser.getName().toLowerCase(); switch (action) { - case "create": + case CASE_CREATE_NODE: caseForBlock = createCase(caseId, modified, userId); isCreateOrUpdate = true; break; - case "update": + case CASE_UPDATE_NODE: caseForBlock = loadCase(caseForBlock, caseId, true); updateCase(caseForBlock, caseId); isCreateOrUpdate = true; break; - case "close": + case CASE_CLOSE_NODE: caseForBlock = loadCase(caseForBlock, caseId, true); closeCase(caseForBlock, caseId); break; - case "index": + case CASE_INDEX_NODE: caseForBlock = loadCase(caseForBlock, caseId, false); indexCase(caseForBlock, caseId); break; - case "attachment": + case CASE_ATTACHMENT_NODE: caseForBlock = loadCase(caseForBlock, caseId, false); processCaseAttachment(caseForBlock); break; @@ -135,7 +143,7 @@ private Case createCase(String caseId, Date modified, String userId) throws Inva String[] data = new String[3]; Case caseForBlock = null; - while (nextTagInBlock("create")) { + while (nextTagInBlock(CASE_CREATE_NODE)) { String tag = parser.getName(); switch (tag) { case CASE_PROPERTY_CASE_TYPE: @@ -188,7 +196,7 @@ private Case createCase(String caseId, Date modified, String userId) throws Inva } private void updateCase(Case caseForBlock, String caseId) throws InvalidStructureException, IOException, XmlPullParserException { - while (nextTagInBlock("update")) { + while (nextTagInBlock(CASE_UPDATE_NODE)) { String key = parser.getName(); String value = parser.nextText().trim(); @@ -244,11 +252,11 @@ private void closeCase(Case caseForBlock, String caseId) throws IOException { } private void indexCase(Case caseForBlock, String caseId) throws InvalidStructureException, IOException, XmlPullParserException { - while (nextTagInBlock("index")) { + while (nextTagInBlock(CASE_INDEX_NODE)) { String indexName = parser.getName(); - String caseType = parser.getAttributeValue(null, "case_type"); + String caseType = parser.getAttributeValue(null, CASE_PROPERTY_INDEX_CASE_TYPE); - String relationship = parser.getAttributeValue(null, "relationship"); + String relationship = parser.getAttributeValue(null, CASE_PROPERTY_INDEX_RELATIONSHIP); if (relationship == null) { relationship = CaseIndex.RELATIONSHIP_CHILD; } else if ("".equals(relationship)) { @@ -278,11 +286,11 @@ private void indexCase(Case caseForBlock, String caseId) throws InvalidStructure } private void processCaseAttachment(Case caseForBlock) throws InvalidStructureException, IOException, XmlPullParserException { - while (nextTagInBlock("attachment")) { + while (nextTagInBlock(CASE_ATTACHMENT_NODE)) { String attachmentName = parser.getName(); - String src = parser.getAttributeValue(null, "src"); - String from = parser.getAttributeValue(null, "from"); - String fileName = parser.getAttributeValue(null, "name"); + String src = parser.getAttributeValue(null, CASE_PROPERTY_ATTACHMENT_SRC); + String from = parser.getAttributeValue(null, CASE_PROPERTY_ATTACHMENT_FROM); + String fileName = parser.getAttributeValue(null, CASE_PROPERTY_ATTACHMENT_NAME); if ((src == null || "".equals(src)) && (from == null || "".equals(from))) { //this is actually an attachment removal diff --git a/src/main/java/org/commcare/xml/CaseXmlParserUtil.java b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java index 18a7b32d4d..d414a4cdf6 100644 --- a/src/main/java/org/commcare/xml/CaseXmlParserUtil.java +++ b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java @@ -6,7 +6,12 @@ import org.kxml2.io.KXmlParser; public class CaseXmlParserUtil { - public static final String CASE_NODE_NAME = "case"; + public static final String CASE_NODE = "case"; + public static final String CASE_CREATE_NODE = "create"; + public static final String CASE_UPDATE_NODE = "update"; + public static final String CASE_CLOSE_NODE = "close"; + public static final String CASE_INDEX_NODE = "index"; + public static final String CASE_ATTACHMENT_NODE = "attachment"; public static final String CASE_PROPERTY_CASE_ID = "case_id"; public static final String CASE_PROPERTY_CASE_TYPE = "case_type"; public static final String CASE_PROPERTY_OWNER_ID = "owner_id"; @@ -20,10 +25,11 @@ public class CaseXmlParserUtil { public static final String CASE_PROPERTY_CATEGORY = "category"; public static final String CASE_PROPERTY_STATE = "state"; public static final String CASE_PROPERTY_INDEX = "index"; - public static final String ATTACHMENT_FROM_LOCAL = "local"; - public static final String ATTACHMENT_FROM_REMOTE = "remote"; - public static final String ATTACHMENT_FROM_INLINE = "inline"; - public static final String CASE_XML_NAMESPACE = "http://commcarehq.org/case/transaction/v2"; + public static final String CASE_PROPERTY_INDEX_CASE_TYPE = "case_type"; + public static final String CASE_PROPERTY_INDEX_RELATIONSHIP = "relationship"; + public static final String CASE_PROPERTY_ATTACHMENT_SRC = "src"; + public static final String CASE_PROPERTY_ATTACHMENT_FROM = "from"; + public static final String CASE_PROPERTY_ATTACHMENT_NAME = "name"; public static void validateMandatoryProperty(String key, Object value, String caseId, KXmlParser parser) throws diff --git a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java index 6ae91d3037..fea035e3ba 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java @@ -1,5 +1,6 @@ package org.commcare.xml.bulk; +import static org.commcare.xml.CaseXmlParserUtil.CASE_NODE; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_NAME; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_TYPE; @@ -7,6 +8,8 @@ import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_DATE_OPENED; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_EXTERNAL_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_INDEX; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_INDEX_CASE_TYPE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_INDEX_RELATIONSHIP; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_LAST_MODIFIED; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_OWNER_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATE; @@ -50,13 +53,13 @@ public BulkCaseInstanceXmlParser(KXmlParser parser, IStorageUtilityIndexed @Override protected void requestModelReadsForElement(TreeElement bufferedTreeElement, Set currentBulkReadSet) { - String caseId = bufferedTreeElement.getAttributeValue(null, "case_id"); + String caseId = bufferedTreeElement.getAttributeValue(null, CASE_PROPERTY_CASE_ID); currentBulkReadSet.add(caseId); } @Override protected void preParseValidate() throws InvalidStructureException { - checkNode("case"); + checkNode(CASE_NODE); } @Override @@ -184,10 +187,10 @@ private static void indexCase(TreeElement indexElement, Case caseForBlock, Strin TreeElement subElement = indexElement.getChildAt(i); String indexName = subElement.getName(); - String caseType = subElement.getAttributeValue(null, "case_type"); + String caseType = subElement.getAttributeValue(null, CASE_PROPERTY_INDEX_CASE_TYPE); String value = getTrimmedElementTextOrBlank(subElement); - String relationship = subElement.getAttributeValue(null, "relationship"); + String relationship = subElement.getAttributeValue(null, CASE_PROPERTY_INDEX_RELATIONSHIP); if (relationship == null) { relationship = CaseIndex.RELATIONSHIP_CHILD; } else if ("".equals(relationship)) { diff --git a/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java index 22a233c0f6..e32d3c9991 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java @@ -1,5 +1,13 @@ package org.commcare.xml.bulk; +import static org.commcare.xml.CaseXmlParserUtil.CASE_ATTACHMENT_NODE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_CLOSE_NODE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_CREATE_NODE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_INDEX_NODE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_NODE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_ATTACHMENT_FROM; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_ATTACHMENT_NAME; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_ATTACHMENT_SRC; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_NAME; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_CASE_TYPE; @@ -7,9 +15,12 @@ import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_DATE_MODIFIED; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_DATE_OPENED; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_EXTERNAL_ID; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_INDEX_CASE_TYPE; +import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_INDEX_RELATIONSHIP; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_OWNER_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATE; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_USER_ID; +import static org.commcare.xml.CaseXmlParserUtil.CASE_UPDATE_NODE; import static org.commcare.xml.CaseXmlParserUtil.validateMandatoryProperty; import org.commcare.cases.model.Case; @@ -50,7 +61,7 @@ protected void requestModelReadsForElement(TreeElement bufferedTreeElement, Set< @Override protected void preParseValidate() throws InvalidStructureException { - checkNode("case"); + checkNode(CASE_NODE); } @Override @@ -71,24 +82,24 @@ protected void processBufferedElement(TreeElement bufferedTreeElement, Map Date: Thu, 20 Apr 2023 16:33:17 -0300 Subject: [PATCH 104/402] adds and implements an interface for case index changes --- .../commcare/xml/CaseIndexChangeListener.java | 21 ++++++++++++++++ .../java/org/commcare/xml/CaseXmlParser.java | 14 ++--------- .../xml/bulk/BulkCaseInstanceXmlParser.java | 8 +++++- .../xml/bulk/BulkProcessingCaseXmlParser.java | 25 +++++++------------ 4 files changed, 39 insertions(+), 29 deletions(-) create mode 100644 src/main/java/org/commcare/xml/CaseIndexChangeListener.java diff --git a/src/main/java/org/commcare/xml/CaseIndexChangeListener.java b/src/main/java/org/commcare/xml/CaseIndexChangeListener.java new file mode 100644 index 0000000000..a10b8dad2c --- /dev/null +++ b/src/main/java/org/commcare/xml/CaseIndexChangeListener.java @@ -0,0 +1,21 @@ +package org.commcare.xml; + +/** + * listener for any changes to case indexes due to processing a case transaction + */ +public interface CaseIndexChangeListener { + + + /** + * A signal that notes that processing a transaction has resulted in a + * potential change in what cases should be on the phone. This can be + * due to a case's owner changing, a case closing, an index moving, etc. + * + * Does not have to be consumed, but can be used to identify proactively + * when to reconcile what cases should be available. + * + * @param caseId The ID of a case which has changed in a potentially + * disruptive way + */ + abstract void onIndexDisrupted(String caseId); +} diff --git a/src/main/java/org/commcare/xml/CaseXmlParser.java b/src/main/java/org/commcare/xml/CaseXmlParser.java index 2734c24641..4b7916311a 100644 --- a/src/main/java/org/commcare/xml/CaseXmlParser.java +++ b/src/main/java/org/commcare/xml/CaseXmlParser.java @@ -52,7 +52,7 @@ * * @author ctsims */ -public class CaseXmlParser extends TransactionParser { +public class CaseXmlParser extends TransactionParser implements CaseIndexChangeListener { private final IStorageUtilityIndexed storage; private final boolean acceptCreateOverwrites; @@ -335,17 +335,7 @@ public IStorageUtilityIndexed storage() { return storage; } - /** - * A signal that notes that processing a transaction has resulted in a - * potential change in what cases should be on the phone. This can be - * due to a case's owner changing, a case closing, an index moving, etc. - * - * Does not have to be consumed, but can be used to identify proactively - * when to reconcile what cases should be available. - * - * @param caseId The ID of a case which has changed in a potentially - * disruptive way - */ + @Override public void onIndexDisrupted(String caseId) { } diff --git a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java index fea035e3ba..1d776fee06 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java @@ -19,6 +19,7 @@ import org.commcare.cases.model.Case; import org.commcare.cases.model.CaseIndex; import org.commcare.modern.engine.cases.CaseIndexTable; +import org.commcare.xml.CaseIndexChangeListener; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.model.utils.DateUtils; import org.javarosa.core.services.storage.IStorageUtilityIndexed; @@ -39,7 +40,7 @@ */ // todo this and other case parsers duplicates a bunch of logic today that can be unified -public class BulkCaseInstanceXmlParser extends BulkElementParser { +public class BulkCaseInstanceXmlParser extends BulkElementParser implements CaseIndexChangeListener { private final CaseIndexTable mCaseIndexTable; private final IStorageUtilityIndexed storage; @@ -219,4 +220,9 @@ private static void indexCase(TreeElement indexElement, Case caseForBlock, Strin } } } + + @Override + public void onIndexDisrupted(String caseId) { + + } } diff --git a/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java index e32d3c9991..1baee215b8 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java @@ -25,6 +25,7 @@ import org.commcare.cases.model.Case; import org.commcare.cases.model.CaseIndex; +import org.commcare.xml.CaseIndexChangeListener; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.model.utils.DateUtils; import org.javarosa.core.util.externalizable.SerializationLimitationException; @@ -47,7 +48,8 @@ * * Created by ctsims on 3/14/2017. */ -public abstract class BulkProcessingCaseXmlParser extends BulkElementParser { +public abstract class BulkProcessingCaseXmlParser extends BulkElementParser implements + CaseIndexChangeListener { public BulkProcessingCaseXmlParser(KXmlParser parser) { super(parser); @@ -97,7 +99,7 @@ protected void processBufferedElement(TreeElement bufferedTreeElement, Map Date: Thu, 20 Apr 2023 16:44:00 -0300 Subject: [PATCH 105/402] move getTrimmedElementTextOrBlank to util --- .../java/org/commcare/xml/CaseXmlParserUtil.java | 14 ++++++++++++++ .../xml/bulk/BulkCaseInstanceXmlParser.java | 8 +------- .../xml/bulk/BulkProcessingCaseXmlParser.java | 11 ++--------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/commcare/xml/CaseXmlParserUtil.java b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java index d414a4cdf6..7bb5cb8195 100644 --- a/src/main/java/org/commcare/xml/CaseXmlParserUtil.java +++ b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java @@ -1,6 +1,7 @@ package org.commcare.xml; import org.commcare.cases.model.Case; +import org.javarosa.core.model.instance.TreeElement; import org.javarosa.xml.util.InvalidCasePropertyLengthException; import org.javarosa.xml.util.InvalidStructureException; import org.kxml2.io.KXmlParser; @@ -59,4 +60,17 @@ private static int getStringLength(String input) { return input != null ? input.length() : 0; } + /** + * Trims and returns elements value or empty string + * @param element TreeElement we want the value for + * @return empty string if element value is null, otherwise the trimmed value for element + */ + public static String getTrimmedElementTextOrBlank(TreeElement element) { + if (element.getValue() == null) { + return ""; + } + + return element.getValue().uncast().getString().trim(); + } + } diff --git a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java index 1d776fee06..f122c4aa92 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java @@ -14,6 +14,7 @@ import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_OWNER_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATE; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATUS; +import static org.commcare.xml.CaseXmlParserUtil.getTrimmedElementTextOrBlank; import static org.commcare.xml.CaseXmlParserUtil.validateMandatoryProperty; import org.commcare.cases.model.Case; @@ -108,13 +109,6 @@ private void validateCase(Case caseForBlock) throws InvalidStructureException { parser); } - private static String getTrimmedElementTextOrBlank(TreeElement element) { - if (element.getValue() == null) { - return ""; - } - return element.getValue().uncast().getString().trim(); - } - protected Case buildCase(String name, String typeId) { return new Case(name, typeId); } diff --git a/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java index 1baee215b8..62232829bb 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java @@ -21,6 +21,7 @@ import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATE; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_USER_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_UPDATE_NODE; +import static org.commcare.xml.CaseXmlParserUtil.getTrimmedElementTextOrBlank; import static org.commcare.xml.CaseXmlParserUtil.validateMandatoryProperty; import org.commcare.cases.model.Case; @@ -127,14 +128,6 @@ protected void processBufferedElement(TreeElement bufferedTreeElement, Map currentOperatingSet, String caseId, Date modified, String userId) throws InvalidStructureException { @@ -268,7 +261,7 @@ private static void indexCase(TreeElement indexElement, Case caseForBlock, Strin relationship = CaseIndex.RELATIONSHIP_CHILD; } - String value = this.getTrimmedElementTextOrBlank(subElement); + String value = getTrimmedElementTextOrBlank(subElement); if (value.equals(caseId)) { throw new ActionableInvalidStructureException("case.error.self.index", new String[]{caseId}, "Case " + caseId + " cannot index itself"); From e0242eef5195ff17970020cca54cb35bf5209375 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 20 Apr 2023 16:48:35 -0300 Subject: [PATCH 106/402] reorganise indexCase to club if conditions for same variable --- .../xml/bulk/BulkProcessingCaseXmlParser.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java index 62232829bb..a54773ce5a 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java @@ -256,24 +256,23 @@ private static void indexCase(TreeElement indexElement, Case caseForBlock, Strin String indexName = subElement.getName(); String caseType = subElement.getAttributeValue(null, CASE_PROPERTY_INDEX_CASE_TYPE); - String relationship = subElement.getAttributeValue(null, CASE_PROPERTY_INDEX_RELATIONSHIP); - if (relationship == null) { - relationship = CaseIndex.RELATIONSHIP_CHILD; - } - String value = getTrimmedElementTextOrBlank(subElement); - if (value.equals(caseId)) { - throw new ActionableInvalidStructureException("case.error.self.index", new String[]{caseId}, "Case " + caseId + " cannot index itself"); - } - - //Remove any ambiguity associated with empty values - if (value.equals("")) { + throw new ActionableInvalidStructureException("case.error.self.index", new String[]{caseId}, + "Case " + caseId + " cannot index itself"); + } else if (value.equals("")) { + //Remove any ambiguity associated with empty values value = null; } - if ("".equals(relationship)) { - throw new InvalidStructureException(String.format("Invalid Case Transaction for Case[%s]: Attempt to add a '' relationship type to entity[%s]", caseId, value)); + String relationship = subElement.getAttributeValue(null, CASE_PROPERTY_INDEX_RELATIONSHIP); + if (relationship == null) { + relationship = CaseIndex.RELATIONSHIP_CHILD; + } else if ("".equals(relationship)) { + throw new InvalidStructureException(String.format( + "Invalid Case Transaction for Case[%s]: Attempt to add a '' relationship type to " + + "entity[%s]", + caseId, value)); } From 92828bb2f7cbd60822ae9a60704e2a542bf2ca32 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 20 Apr 2023 16:56:23 -0300 Subject: [PATCH 107/402] move index Case to util --- .../org/commcare/xml/CaseXmlParserUtil.java | 53 +++++++++++++++++++ .../xml/bulk/BulkCaseInstanceXmlParser.java | 46 +--------------- .../xml/bulk/BulkProcessingCaseXmlParser.java | 48 +---------------- 3 files changed, 56 insertions(+), 91 deletions(-) diff --git a/src/main/java/org/commcare/xml/CaseXmlParserUtil.java b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java index 7bb5cb8195..187b1a25ad 100644 --- a/src/main/java/org/commcare/xml/CaseXmlParserUtil.java +++ b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java @@ -1,7 +1,9 @@ package org.commcare.xml; import org.commcare.cases.model.Case; +import org.commcare.cases.model.CaseIndex; import org.javarosa.core.model.instance.TreeElement; +import org.javarosa.xml.util.ActionableInvalidStructureException; import org.javarosa.xml.util.InvalidCasePropertyLengthException; import org.javarosa.xml.util.InvalidStructureException; import org.kxml2.io.KXmlParser; @@ -73,4 +75,55 @@ public static String getTrimmedElementTextOrBlank(TreeElement element) { return element.getValue().uncast().getString().trim(); } + /** + * Processes given treeElement to set case indexes + * @param indexElement TreeElement containing the index nodes + * @param caseForBlock Case to which indexes should be applied + * @param caseId id of the given case + * @param caseIndexChangeListener listener for the case index changes + * @throws InvalidStructureException thrown when the given indexElement doesn't have valid index nodes + */ + public static void indexCase(TreeElement indexElement, Case caseForBlock, String caseId, + CaseIndexChangeListener caseIndexChangeListener) + throws InvalidStructureException { + for (int i = 0; i < indexElement.getNumChildren(); i++) { + TreeElement subElement = indexElement.getChildAt(i); + + String indexName = subElement.getName(); + String caseType = subElement.getAttributeValue(null, CASE_PROPERTY_INDEX_CASE_TYPE); + + String value = getTrimmedElementTextOrBlank(subElement); + if (value.equals(caseId)) { + throw new ActionableInvalidStructureException("case.error.self.index", new String[]{caseId}, + "Case " + caseId + " cannot index itself"); + } else if (value.equals("")) { + //Remove any ambiguity associated with empty values + value = null; + } + + String relationship = subElement.getAttributeValue(null, CASE_PROPERTY_INDEX_RELATIONSHIP); + if (relationship == null) { + relationship = CaseIndex.RELATIONSHIP_CHILD; + } else if ("".equals(relationship)) { + throw new InvalidStructureException(String.format( + "Invalid Case Transaction for Case[%s]: Attempt to add a '' relationship type to " + + "entity[%s]", + caseId, value)); + } + + + //Process blank inputs in the same manner as data fields (IE: Remove the underlying model) + if (value == null) { + if (caseForBlock.removeIndex(indexName)) { + caseIndexChangeListener.onIndexDisrupted(caseId); + } + } else { + if (caseForBlock.setIndex(new CaseIndex(indexName, caseType, value, + relationship))) { + caseIndexChangeListener.onIndexDisrupted(caseId); + } + } + } + } + } diff --git a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java index f122c4aa92..e5d90f6ce5 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkCaseInstanceXmlParser.java @@ -8,24 +8,21 @@ import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_DATE_OPENED; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_EXTERNAL_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_INDEX; -import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_INDEX_CASE_TYPE; -import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_INDEX_RELATIONSHIP; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_LAST_MODIFIED; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_OWNER_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATE; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATUS; import static org.commcare.xml.CaseXmlParserUtil.getTrimmedElementTextOrBlank; +import static org.commcare.xml.CaseXmlParserUtil.indexCase; import static org.commcare.xml.CaseXmlParserUtil.validateMandatoryProperty; import org.commcare.cases.model.Case; -import org.commcare.cases.model.CaseIndex; import org.commcare.modern.engine.cases.CaseIndexTable; import org.commcare.xml.CaseIndexChangeListener; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.model.utils.DateUtils; import org.javarosa.core.services.storage.IStorageUtilityIndexed; import org.javarosa.core.util.externalizable.SerializationLimitationException; -import org.javarosa.xml.util.ActionableInvalidStructureException; import org.javarosa.xml.util.InvalidStructureException; import org.kxml2.io.KXmlParser; @@ -142,7 +139,7 @@ private void updateCase(TreeElement updateElement, caseForBlock.setState(value); break; case CASE_PROPERTY_INDEX: - indexCase(subElement, caseForBlock, caseId); + indexCase(subElement, caseForBlock, caseId, this); break; default: caseForBlock.setProperty(key, value); @@ -176,45 +173,6 @@ protected void performBulkWrite(LinkedHashMap writeLog) throws IOE } } - private static void indexCase(TreeElement indexElement, Case caseForBlock, String caseId) - throws InvalidStructureException { - for (int i = 0; i < indexElement.getNumChildren(); i++) { - TreeElement subElement = indexElement.getChildAt(i); - - String indexName = subElement.getName(); - String caseType = subElement.getAttributeValue(null, CASE_PROPERTY_INDEX_CASE_TYPE); - - String value = getTrimmedElementTextOrBlank(subElement); - String relationship = subElement.getAttributeValue(null, CASE_PROPERTY_INDEX_RELATIONSHIP); - if (relationship == null) { - relationship = CaseIndex.RELATIONSHIP_CHILD; - } else if ("".equals(relationship)) { - throw new InvalidStructureException(String.format( - "Invalid Case Transaction for Case[%s]: Attempt to add a '' relationship type to " - + "entity[%s]", - caseId, value)); - } - - if (value.equals(caseId)) { - throw new ActionableInvalidStructureException("case.error.self.index", new String[]{caseId}, - "Case " + caseId + " cannot index itself"); - } - - //Remove any ambiguity associated with empty values - if (value.equals("")) { - value = null; - } - - //Process blank inputs in the same manner as data fields (IE: Remove the underlying model) - if (value == null) { - caseForBlock.removeIndex(indexName); - } else { - caseForBlock.setIndex(new CaseIndex(indexName, caseType, value, - relationship)); - } - } - } - @Override public void onIndexDisrupted(String caseId) { diff --git a/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java b/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java index a54773ce5a..7056db104c 100644 --- a/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java +++ b/src/main/java/org/commcare/xml/bulk/BulkProcessingCaseXmlParser.java @@ -15,22 +15,19 @@ import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_DATE_MODIFIED; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_DATE_OPENED; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_EXTERNAL_ID; -import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_INDEX_CASE_TYPE; -import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_INDEX_RELATIONSHIP; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_OWNER_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_STATE; import static org.commcare.xml.CaseXmlParserUtil.CASE_PROPERTY_USER_ID; import static org.commcare.xml.CaseXmlParserUtil.CASE_UPDATE_NODE; import static org.commcare.xml.CaseXmlParserUtil.getTrimmedElementTextOrBlank; +import static org.commcare.xml.CaseXmlParserUtil.indexCase; import static org.commcare.xml.CaseXmlParserUtil.validateMandatoryProperty; import org.commcare.cases.model.Case; -import org.commcare.cases.model.CaseIndex; import org.commcare.xml.CaseIndexChangeListener; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.model.utils.DateUtils; import org.javarosa.core.util.externalizable.SerializationLimitationException; -import org.javarosa.xml.util.ActionableInvalidStructureException; import org.javarosa.xml.util.InvalidStructureException; import org.kxml2.io.KXmlParser; @@ -247,49 +244,6 @@ private void closeCase(Case caseForBlock, String caseId) { onIndexDisrupted(caseId); } - private static void indexCase(TreeElement indexElement, Case caseForBlock, String caseId, - CaseIndexChangeListener caseIndexChangeListener) - throws InvalidStructureException { - for (int i = 0; i < indexElement.getNumChildren(); i++) { - TreeElement subElement = indexElement.getChildAt(i); - - String indexName = subElement.getName(); - String caseType = subElement.getAttributeValue(null, CASE_PROPERTY_INDEX_CASE_TYPE); - - String value = getTrimmedElementTextOrBlank(subElement); - if (value.equals(caseId)) { - throw new ActionableInvalidStructureException("case.error.self.index", new String[]{caseId}, - "Case " + caseId + " cannot index itself"); - } else if (value.equals("")) { - //Remove any ambiguity associated with empty values - value = null; - } - - String relationship = subElement.getAttributeValue(null, CASE_PROPERTY_INDEX_RELATIONSHIP); - if (relationship == null) { - relationship = CaseIndex.RELATIONSHIP_CHILD; - } else if ("".equals(relationship)) { - throw new InvalidStructureException(String.format( - "Invalid Case Transaction for Case[%s]: Attempt to add a '' relationship type to " - + "entity[%s]", - caseId, value)); - } - - - //Process blank inputs in the same manner as data fields (IE: Remove the underlying model) - if (value == null) { - if (caseForBlock.removeIndex(indexName)) { - caseIndexChangeListener.onIndexDisrupted(caseId); - } - } else { - if (caseForBlock.setIndex(new CaseIndex(indexName, caseType, value, - relationship))) { - caseIndexChangeListener.onIndexDisrupted(caseId); - } - } - } - } - @Override public void onIndexDisrupted(String caseId) { From f786913338c3c3a2a6bf269d22b138f2896e79a1 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 26 Apr 2023 11:08:45 -0300 Subject: [PATCH 108/402] Simplify instructions for Android testing --- .github/contributing.md | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/contributing.md b/.github/contributing.md index 817abeaf97..92c9466d8e 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -8,28 +8,22 @@ We try to keep both these branches in sync with each other the best we can. To d 1. If you are working on a Formplayer change, you will want to start by checking out `your_feature_branch` from `formplayer` as the base branch. Make changes on `your_feature_branch` and create your original PR against `formplayer` branch. -2. Now you will need to duplicate this PR by making another PR against `master`. Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. +2. Now you will need to duplicate this PR by making another PR against `master`. Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. This should result in a Github Actions workflow duplicating your PR against `master`. Go to the duplicate PR, close and re-open it to run the Github checks against it. -3. In order for us to test that your PR against `master` doesn't break anything on CommCare Android, we need to run android side tests with your PR. +3. In order for us to test that your PR against `master` doesn't break anything on CommCare Android, you need to run android side tests with your PR. To do this - - - Check out a new branch say `test_cc_1189` from `master` in [CommCare Android](https://github.com/dimagi/commcare-android) - - Do an empty commit on your new branch as `git commit --allow-empty -m "Empty-Commit"` and push your branch to github. - - Create a dummy test PR with a subject `[Test] CC 1189`. - - You can remove all the fields from the PR template here and add a line at the very bottom saying `cross-request: link_to_commcare-core_master_pr`. Without this line the tests won't factor in your CommCare Core `master` PR to run tests with. - - Label the PR with `cross requested tag` - - Label the PR with an appropriate `product/*` tag. - - You don't need to add any reviewers here as this PR will be closed without merging and is only used to run Android tests + - Go to the [Android test PR](https://github.com/dimagi/commcare-android/pull/2612) + - Change "cross-request" PR in description to the duplicate PR created in step 2 above + - Make a comment saying `@damagatchi retest this please` ([example](https://github.com/dimagi/commcare-android/pull/2612#issuecomment-1453132625)) - See https://github.com/dimagi/commcare-android/pull/2609 for an example of such a test PR. - -4. Go back to your CommCare Core `master` PR and add `cross-request: link_to_commcare-android_test_pr` as the last line in the PR description. This PR will now be owned by the Android devs to decide whether any additional changes are required on CommCare Android before merging this PR. +4. Request a review from `@shubham1g5`on your duplicate commcare-core PR. Android devs will now be responsible to make any further Android compatibility changes for this PR if required and merge it. ##### Duplicating A CommCare Android change to Formplayer 1. If you are working on a CommCare Android change, you will want to start by checking out `your_feature_branch` from `master` as the base branch. Make changes on `your_feature_branch` and create your original PR against `master` branch. -2. Now you will need to duplicate this PR by making another PR against `formplayer`. Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. +2. Now you will need to duplicate this PR by making another PR against `formplayer`. Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. This should result in a Github Actions workflow duplicating your PR against `formplayer`. Go to the duplicate PR, close and re-open it to run the Github checks against it. 3. In order for us to test that your PR against `formplayer` doesn't break anything on Formplayer, we need to run formplayer side tests with your PR. To do this - From fd1fc5ac86ecb4fc781fbc87bbb9513f7d367f1c Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 26 Apr 2023 12:09:35 -0300 Subject: [PATCH 109/402] adds back constants used on Android --- src/main/java/org/commcare/xml/CaseXmlParserUtil.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/xml/CaseXmlParserUtil.java b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java index 187b1a25ad..4c82761033 100644 --- a/src/main/java/org/commcare/xml/CaseXmlParserUtil.java +++ b/src/main/java/org/commcare/xml/CaseXmlParserUtil.java @@ -33,7 +33,9 @@ public class CaseXmlParserUtil { public static final String CASE_PROPERTY_ATTACHMENT_SRC = "src"; public static final String CASE_PROPERTY_ATTACHMENT_FROM = "from"; public static final String CASE_PROPERTY_ATTACHMENT_NAME = "name"; - + public static final String ATTACHMENT_FROM_LOCAL = "local"; + public static final String ATTACHMENT_FROM_REMOTE = "remote"; + public static final String CASE_XML_NAMESPACE = "http://commcarehq.org/case/transaction/v2"; public static void validateMandatoryProperty(String key, Object value, String caseId, KXmlParser parser) throws InvalidStructureException { From 6b3b8fed31b6ca5de005965e734bc2b17f8e99f8 Mon Sep 17 00:00:00 2001 From: Jennifer Schweers Date: Tue, 9 May 2023 16:41:03 -0400 Subject: [PATCH 110/402] Added address display format --- src/main/java/org/commcare/suite/model/Style.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/commcare/suite/model/Style.java b/src/main/java/org/commcare/suite/model/Style.java index 7104b63cee..86968bfcfb 100644 --- a/src/main/java/org/commcare/suite/model/Style.java +++ b/src/main/java/org/commcare/suite/model/Style.java @@ -36,6 +36,7 @@ enum DisplayFormat { Image, Audio, Text, + Address, Graph, Phone, Markdown, @@ -76,6 +77,9 @@ private void setDisplayFormatFromString(String displayFormat){ case "text": setDisplayFormat(DisplayFormat.Text); break; + case "address": + setDisplayFormat(DisplayFormat.Address); + break; case "graph": setDisplayFormat(DisplayFormat.Graph); break; From 709a89e9ecf67789e1ec55f39758b84c76546d49 Mon Sep 17 00:00:00 2001 From: Simon Kelly Date: Fri, 12 May 2023 15:00:47 +0200 Subject: [PATCH 111/402] refactor EntityScreen to expose 'getLongDetail' method --- .../commcare/util/screen/EntityScreen.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityScreen.java b/src/cli/java/org/commcare/util/screen/EntityScreen.java index c410e6f14d..8b3c63b8c3 100755 --- a/src/cli/java/org/commcare/util/screen/EntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreen.java @@ -349,26 +349,27 @@ public void setCurrentScreenToDetail(int index) throws CommCareSessionException } public Detail[] getLongDetailList(TreeReference ref) { - Detail[] longDetailList; - String longDetailId = this.mNeededDatum.getLongDetail(); - if (longDetailId == null) { - return null; - } - Detail d = mPlatform.getDetail(longDetailId); - if (d == null) { - return null; - } + Detail d = getLongDetail(); + if (d == null) return null; EvaluationContext contextForChildDetailDisplayConditions = EntityUtil.prepareCompoundEvaluationContext(ref, d, evalContext); - longDetailList = d.getDisplayableChildDetails(contextForChildDetailDisplayConditions); + Detail[] longDetailList = d.getDisplayableChildDetails(contextForChildDetailDisplayConditions); if (longDetailList == null || longDetailList.length == 0) { longDetailList = new Detail[]{d}; } return longDetailList; } + public Detail getLongDetail() { + String longDetailId = this.mNeededDatum.getLongDetail(); + if (longDetailId == null) { + return null; + } + return mPlatform.getDetail(longDetailId); + } + public String[] getDetailListTitles(EvaluationContext subContext, TreeReference reference) { Detail[] mLongDetailList = getLongDetailList(reference); String[] titles = new String[mLongDetailList.length]; From a84c80086cefc3bee4899e9764a6a7ef3664acb3 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 3 May 2023 14:54:30 -0300 Subject: [PATCH 112/402] attach query response to entity list screen --- .../java/org/commcare/util/screen/EntityScreen.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/cli/java/org/commcare/util/screen/EntityScreen.java b/src/cli/java/org/commcare/util/screen/EntityScreen.java index 8b3c63b8c3..4e8b4ec365 100755 --- a/src/cli/java/org/commcare/util/screen/EntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreen.java @@ -63,6 +63,8 @@ public class EntityScreen extends CompoundScreenHost { private boolean initialized = false; private Action autoLaunchAction; + private QueryScreen queryScreen; + public EntityScreen(boolean handleCaseIndex) { this.handleCaseIndex = handleCaseIndex; } @@ -468,4 +470,12 @@ public boolean evalAndExecuteAutoLaunchAction(String nextInput, CommCareSession } return false; } + + public QueryScreen getQueryScreen() { + return queryScreen; + } + + public void setQueryScreen(QueryScreen queryScreen) { + this.queryScreen = queryScreen; + } } From e140dd0ff9f174245da9aa57642806bdd544bc6b Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 15 May 2023 20:38:25 -0400 Subject: [PATCH 113/402] Adds a getter for session in Query Screen --- src/cli/java/org/commcare/util/screen/QueryScreen.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/QueryScreen.java b/src/cli/java/org/commcare/util/screen/QueryScreen.java index f0fcd2fad7..877c691cfe 100644 --- a/src/cli/java/org/commcare/util/screen/QueryScreen.java +++ b/src/cli/java/org/commcare/util/screen/QueryScreen.java @@ -7,7 +7,6 @@ import static org.commcare.suite.model.QueryPrompt.INPUT_TYPE_SELECT; import static org.commcare.suite.model.QueryPrompt.INPUT_TYPE_SELECT1; -import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.Multimap; import org.commcare.cases.util.StringUtils; @@ -20,7 +19,6 @@ import org.commcare.session.RemoteQuerySessionManager; import org.commcare.suite.model.RemoteQueryDatum; import org.commcare.suite.model.QueryPrompt; -import org.commcare.suite.model.RemoteQueryDatum; import org.javarosa.core.model.SelectChoice; import org.javarosa.core.model.instance.ExternalDataInstance; import org.javarosa.core.model.instance.ExternalDataInstanceSource; @@ -28,7 +26,6 @@ import org.javarosa.core.util.NoLocalizedTextException; import org.javarosa.core.util.OrderedHashtable; -import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.net.URL; @@ -305,6 +302,10 @@ public RemoteQueryDatum getQueryDatum() { return remoteQuerySessionManager.getQueryDatum(); } + public SessionWrapper getSession() { + return sessionWrapper; + } + @Override public String toString() { return "QueryScreen[" + mTitle + "]"; From 84119575b90608d4a6f0a5e0156b4324103eef0b Mon Sep 17 00:00:00 2001 From: Dave Viggiano Date: Wed, 24 May 2023 11:34:49 -0400 Subject: [PATCH 114/402] Added BasicAuth subclass of AuthInfo, to support basic HTTP auth. --- src/main/java/org/commcare/core/network/AuthInfo.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/commcare/core/network/AuthInfo.java b/src/main/java/org/commcare/core/network/AuthInfo.java index b2115b71a1..26a65906a1 100644 --- a/src/main/java/org/commcare/core/network/AuthInfo.java +++ b/src/main/java/org/commcare/core/network/AuthInfo.java @@ -19,7 +19,13 @@ public ProvidedAuth(String username, String password) { this.username = username; this.password = password; } + } + public static class BasicAuth extends AuthInfo { + public BasicAuth(String username, String password) { + this.username = username; + this.password = password; + } } // Auth with the currently-logged in user From 97cf7bda6956ef8a5a80c690e09cd57e86493591 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 18 May 2023 20:50:57 -0400 Subject: [PATCH 115/402] getRows is not accessed outside the class --- src/cli/java/org/commcare/util/screen/EntityListSubscreen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java index 360e3b3fea..6a807ac97c 100755 --- a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java @@ -52,7 +52,7 @@ public EntityListSubscreen(Detail shortDetail, Vector references, rows = getRows(mChoices, context, shortDetail); } - public static String[] getRows(TreeReference[] references, + private static String[] getRows(TreeReference[] references, EvaluationContext evaluationContext, Detail detail) { String[] rows = new String[references.length]; From c87a6e8faf5a77bf264bdea1fd10ddbd2297bc6c Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 18 May 2023 20:55:46 -0400 Subject: [PATCH 116/402] rename mChoices -> entitiesRefs --- .../util/screen/EntityListSubscreen.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java index 6a807ac97c..f62e845db5 100755 --- a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java @@ -6,7 +6,6 @@ import org.commcare.suite.model.Action; import org.commcare.suite.model.Detail; import org.commcare.suite.model.DetailField; -import org.commcare.util.screen.MultiSelectEntityScreen; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.model.trace.AccumulatingReporter; @@ -29,7 +28,7 @@ public class EntityListSubscreen extends Subscreen { private static final int SCREEN_WIDTH = 100; - private final TreeReference[] mChoices; + private final TreeReference[] entitiesRefs; private final String[] rows; private final String mHeader; @@ -45,11 +44,11 @@ public EntityListSubscreen(Detail shortDetail, Vector references, mHeader = createHeader(shortDetail, context); this.shortDetail = shortDetail; this.rootContext = context; - this.mChoices = new TreeReference[references.size()]; + this.entitiesRefs = new TreeReference[references.size()]; this.handleCaseIndex = handleCaseIndex; - references.copyInto(mChoices); + references.copyInto(entitiesRefs); actions = shortDetail.getCustomActions(context); - rows = getRows(mChoices, context, shortDetail); + rows = getRows(entitiesRefs, context, shortDetail); } private static String[] getRows(TreeReference[] references, @@ -177,11 +176,11 @@ private static String createHeader(Detail shortDetail, EvaluationContext context @Override public void prompt(PrintStream out) { - int maxLength = String.valueOf(mChoices.length).length(); + int maxLength = String.valueOf(entitiesRefs.length).length(); out.println(ScreenUtils.pad("", maxLength + 1) + mHeader); out.println("==========================================================================================="); - for (int i = 0; i < mChoices.length; ++i) { + for (int i = 0; i < entitiesRefs.length; ++i) { String d = rows[i]; out.println(ScreenUtils.pad(String.valueOf(i), maxLength) + ") " + d); } @@ -221,7 +220,7 @@ public boolean handleInputAndUpdateHost(String input, EntityScreen host, boolean String debugArg = input.substring("debug ".length()); try { int chosenDebugIndex = Integer.valueOf(debugArg.trim()); - createRow(this.mChoices[chosenDebugIndex], rootContext, shortDetail); + createRow(this.entitiesRefs[chosenDebugIndex], rootContext, shortDetail); } catch (NumberFormatException e) { if ("list".equals(debugArg)) { host.printNodesetExpansionTrace(new AccumulatingReporter()); @@ -244,12 +243,12 @@ public boolean handleInputAndUpdateHost(String input, EntityScreen host, boolean selectedRefs = new TreeReference[selectedValues.length]; for (int i = 0; i < selectedValues.length; i++) { int index = Integer.parseInt(selectedValues[i]); - selectedRefs[i] = mChoices[index]; + selectedRefs[i] = entitiesRefs[index]; } } else { int index = Integer.parseInt(input); selectedRefs = new TreeReference[1]; - selectedRefs[0] = mChoices[index]; + selectedRefs[0] = entitiesRefs[index]; } host.updateSelection(input, selectedRefs); return true; From 5ec7dacd060aa0d65b127aa3bd9b383f6684cbc9 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 18 May 2023 21:02:55 -0400 Subject: [PATCH 117/402] Adds getter for actions --- .../java/org/commcare/util/screen/EntityListSubscreen.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java index f62e845db5..cdcdae026d 100755 --- a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java @@ -275,4 +275,8 @@ private static int getWidthHint(DetailField[] fields, DetailField field) { } return widthHint; } + + public Vector getActions() { + return actions; + } } From 7e222e5bc8ef9ae1cb308a9464ee98d2a97f2944 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 18 May 2023 22:11:01 -0400 Subject: [PATCH 118/402] init subscreen even when needsFullInit is false but with empty references This is done as we are planning to move more code in screen classes from response classes and want the screen classes to be source of truth for FP response beans --- .../org/commcare/util/screen/EntityScreen.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityScreen.java b/src/cli/java/org/commcare/util/screen/EntityScreen.java index 4e8b4ec365..993326f5db 100755 --- a/src/cli/java/org/commcare/util/screen/EntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreen.java @@ -148,13 +148,18 @@ public void init(SessionWrapper session) throws CommCareSessionException { if (!this.setCurrentScreenToDetail()) { readyToSkip = true; } - } else { - // We can simply skip evaluating Detail for entities for a detail screen - Vector entityListReferences = isDetailScreen ? new Vector<>() : references; - mCurrentScreen = new EntityListSubscreen(mShortDetail, entityListReferences, evalContext, - handleCaseIndex); } } + + // if readyToSkip, entity screen will not be displayed. We don't need to init the subscreen + if (!readyToSkip) { + // if isDetailScreen or needsFullInit is not set, + // sub screen is needed to handle actions but we can skip eval refs + Vector entityListReferences = + !needsFullInit || isDetailScreen ? new Vector<>() : references; + mCurrentScreen = new EntityListSubscreen(mShortDetail, entityListReferences, evalContext, + handleCaseIndex); + } initialized = true; } From d13ee1332efe8c73c319b6a35bb38678ab23e471 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 18 May 2023 22:46:34 -0400 Subject: [PATCH 119/402] use nodeEntityFactory to create Rows, introduce entities set in screen --- .../util/screen/EntityListSubscreen.java | 87 +++++++------------ 1 file changed, 33 insertions(+), 54 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java index cdcdae026d..87f1698cf3 100755 --- a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java @@ -2,6 +2,8 @@ import static org.commcare.util.screen.MultiSelectEntityScreen.USE_SELECTED_VALUES; +import org.commcare.cases.entity.Entity; +import org.commcare.cases.entity.NodeEntityFactory; import org.commcare.modern.util.Pair; import org.commcare.suite.model.Action; import org.commcare.suite.model.Detail; @@ -16,6 +18,8 @@ import org.javarosa.xpath.XPathException; import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; import java.util.Vector; /** @@ -38,6 +42,7 @@ public class EntityListSubscreen extends Subscreen { private final EvaluationContext rootContext; private boolean handleCaseIndex; + private List> entities; public EntityListSubscreen(Detail shortDetail, Vector references, EvaluationContext context, boolean handleCaseIndex) throws CommCareSessionException { @@ -48,71 +53,41 @@ public EntityListSubscreen(Detail shortDetail, Vector references, this.handleCaseIndex = handleCaseIndex; references.copyInto(entitiesRefs); actions = shortDetail.getCustomActions(context); - rows = getRows(entitiesRefs, context, shortDetail); + initEntities(context, shortDetail); + rows = getRows(shortDetail); } - private static String[] getRows(TreeReference[] references, - EvaluationContext evaluationContext, - Detail detail) { - String[] rows = new String[references.length]; - int i = 0; - for (TreeReference entity : references) { - rows[i] = createRow(entity, evaluationContext, detail); - ++i; + private void initEntities(EvaluationContext context, Detail shortDetail) { + NodeEntityFactory nodeEntityFactory = new NodeEntityFactory(shortDetail, context); + entities = new ArrayList<>(); + for (TreeReference reference : entitiesRefs) { + entities.add(nodeEntityFactory.getEntity(reference)); } - return rows; - } - - private static String createRow(TreeReference entity, EvaluationContext evaluationContext, Detail detail) { - return createRow(entity, false, evaluationContext, detail); + nodeEntityFactory.prepareEntities(entities); } - private static String createRow(TreeReference entity, - boolean collectDebug, - EvaluationContext evaluationContext, - Detail detail) { - EvaluationContext context = new EvaluationContext(evaluationContext, entity); - EvaluationTraceReporter reporter = new AccumulatingReporter(); - - if (collectDebug) { - context.setDebugModeOn(reporter); + private String[] getRows(Detail detail) { + String[] rows = new String[entities.size()]; + for (int e = 0; e < entities.size(); e++) { + Entity entity = entities.get(e); + rows[e] = createRow(entity, detail); } - detail.populateEvaluationContextVariables(context); - - if (collectDebug) { - InstrumentationUtils.printAndClearTraces(reporter, "Variable Traces"); - } - - DetailField[] fields = detail.getFields(); + return rows; + } + private String createRow(Entity entity, Detail detail) { + Object[] entityFields = entity.getData(); + DetailField[] detailFields = detail.getFields(); StringBuilder row = new StringBuilder(); - XPathException detailFieldException = null; - int i = 0; - for (DetailField field : fields) { - Object o; - try { - o = field.getTemplate().evaluate(context); - } catch (XPathException e) { - o = "error (see output)"; - if (detailFieldException == null) { - detailFieldException = e; - } - } + for (int i = 0; i < entityFields.length; i++) { + Object entityField = entityFields[i]; String s; - if (!(o instanceof String)) { + if (!(entityField instanceof String)) { s = ""; } else { - s = (String)o; + s = (String)entityField; } - row.append(ScreenUtils.pad(s, getWidthHint(fields, field))); - } - - if (detailFieldException != null) { - detailFieldException.printStackTrace(); - } - - if (collectDebug) { - InstrumentationUtils.printAndClearTraces(reporter, "Template Traces:"); + row.append(ScreenUtils.pad(s, getWidthHint(detailFields, detailFields[i]))); } return row.toString(); } @@ -220,7 +195,7 @@ public boolean handleInputAndUpdateHost(String input, EntityScreen host, boolean String debugArg = input.substring("debug ".length()); try { int chosenDebugIndex = Integer.valueOf(debugArg.trim()); - createRow(this.entitiesRefs[chosenDebugIndex], rootContext, shortDetail); + createRow(entities.get(chosenDebugIndex), shortDetail); } catch (NumberFormatException e) { if ("list".equals(debugArg)) { host.printNodesetExpansionTrace(new AccumulatingReporter()); @@ -279,4 +254,8 @@ private static int getWidthHint(DetailField[] fields, DetailField field) { public Vector getActions() { return actions; } + + public List> getEntities() { + return entities; + } } From 21426394c54a8fc23f749746b0924a7d67bc60f8 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 18 May 2023 22:48:28 -0400 Subject: [PATCH 120/402] lazy init rows for CLI to avoid calculating it for non CLI context --- .../java/org/commcare/util/screen/EntityListSubscreen.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java index 87f1698cf3..3986468107 100755 --- a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java @@ -33,7 +33,7 @@ public class EntityListSubscreen extends Subscreen { private static final int SCREEN_WIDTH = 100; private final TreeReference[] entitiesRefs; - private final String[] rows; + private String[] rows; private final String mHeader; private final Vector actions; @@ -54,7 +54,6 @@ public EntityListSubscreen(Detail shortDetail, Vector references, references.copyInto(entitiesRefs); actions = shortDetail.getCustomActions(context); initEntities(context, shortDetail); - rows = getRows(shortDetail); } private void initEntities(EvaluationContext context, Detail shortDetail) { @@ -172,6 +171,9 @@ public void prompt(PrintStream out) { @Override public String[] getOptions() { + if (rows == null) { + rows = getRows(shortDetail); + } return rows; } From 87f33bbee8b7b36c4d261978c5913afcff7d4068 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 19 May 2023 10:44:05 -0400 Subject: [PATCH 121/402] move EntityStringFilterer from FP to core This is an exact copy of EntityStringFilterer from FP and is moved to be able to use it in the entity screen classes --- .../cases/entity/EntityStringFilterer.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/main/java/org/commcare/cases/entity/EntityStringFilterer.java diff --git a/src/main/java/org/commcare/cases/entity/EntityStringFilterer.java b/src/main/java/org/commcare/cases/entity/EntityStringFilterer.java new file mode 100644 index 0000000000..2aa21989d1 --- /dev/null +++ b/src/main/java/org/commcare/cases/entity/EntityStringFilterer.java @@ -0,0 +1,57 @@ +package org.commcare.cases.entity; + +import org.commcare.modern.util.Pair; +import org.commcare.util.EntitySortUtil; +import org.javarosa.core.model.instance.TreeReference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Filter entity list via all string-representable entity fields + */ +public class EntityStringFilterer { + private final String[] searchTerms; + private final ArrayList> matchScores = new ArrayList<>(); + + private final NodeEntityFactory nodeFactory; + protected final List> matchList; + protected final List> fullEntityList; + private final boolean isFuzzySearchEnabled; + + public EntityStringFilterer(String[] searchTerms, + NodeEntityFactory nodeFactory, + List> fullEntityList, + boolean isFuzzySearchEnabled) { + this.isFuzzySearchEnabled = isFuzzySearchEnabled; + this.matchList = new ArrayList<>(); + this.nodeFactory = nodeFactory; + this.fullEntityList = fullEntityList; + this.searchTerms = searchTerms; + if (searchTerms == null || searchTerms.length == 0) { + matchList.addAll(fullEntityList); + } + } + + public List> buildMatchList() { + + while (!nodeFactory.isEntitySetReady()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + Locale currentLocale = Locale.getDefault(); + EntitySortUtil.sortEntities(fullEntityList, + searchTerms, + currentLocale, + isFuzzySearchEnabled, + matchScores, + matchList, + fullEntityList::get); + return matchList; + } +} From 40af5b619381ff6861080adc2af1d0b99c9856f8 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 22 May 2023 10:12:05 -0400 Subject: [PATCH 122/402] Adds a class to hold meta data context about entity screen --- .../util/screen/EntityScreenContext.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/cli/java/org/commcare/util/screen/EntityScreenContext.java diff --git a/src/cli/java/org/commcare/util/screen/EntityScreenContext.java b/src/cli/java/org/commcare/util/screen/EntityScreenContext.java new file mode 100644 index 0000000000..48ac88b2bd --- /dev/null +++ b/src/cli/java/org/commcare/util/screen/EntityScreenContext.java @@ -0,0 +1,70 @@ +package org.commcare.util.screen; + +/** + * Holds essential meta data associated with the entity screen + */ +public class EntityScreenContext { + private final int mOffSet; + private final String mSearchText; + private final int mSortIndex; + private final int mCasesPerPage; + private final String[] mSelectedValues; + + /** + * If requesting a case detail will be a case id, else null. When the case id is given it is used to short + * circuit the normal TreeReference calculation by inserting a predicate that is [@case_id = ] + */ + private final String mDetailSelection; + + private static int DEFAULT_CASES_PER_PAGE = 10; + private final boolean fuzzySearch; + + public EntityScreenContext(int offset, String searchText, int sortIndex, int casesPerPage, + String[] selectedValues, String detailSelection, boolean isFuzzySearch) { + mOffSet = offset; + mSearchText = searchText; + mSortIndex = sortIndex; + mCasesPerPage = casesPerPage; + mSelectedValues = selectedValues; + mDetailSelection = detailSelection; + fuzzySearch = isFuzzySearch; + } + + public EntityScreenContext() { + mOffSet = 0; + mSearchText = null; + mSortIndex = 0; + mCasesPerPage = DEFAULT_CASES_PER_PAGE; + mSelectedValues = null; + mDetailSelection = null; + fuzzySearch = false; + } + + public int getOffSet() { + return mOffSet; + } + + public String getSearchText() { + return mSearchText; + } + + public int getSortIndex() { + return mSortIndex; + } + + public int getCasesPerPage() { + return mCasesPerPage; + } + + public String[] getSelectedValues() { + return mSelectedValues; + } + + public String getDetailSelection() { + return mDetailSelection; + } + + public boolean isFuzzySearch() { + return fuzzySearch; + } +} From d09e86553aee7e47ceac083750ae6419f7457544 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 22 May 2023 10:18:22 -0400 Subject: [PATCH 123/402] Pass entity context to Screen classes and use it to construct the filtered and sorted entity set --- .../commcare/util/cli/ApplicationHost.java | 4 +- .../util/screen/EntityListSubscreen.java | 57 ++++++++++++++++++- .../commcare/util/screen/EntityScreen.java | 16 ++++-- .../util/screen/MultiSelectEntityScreen.java | 5 +- 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src/cli/java/org/commcare/util/cli/ApplicationHost.java b/src/cli/java/org/commcare/util/cli/ApplicationHost.java index 69e7baf4b3..f8367ff96b 100644 --- a/src/cli/java/org/commcare/util/cli/ApplicationHost.java +++ b/src/cli/java/org/commcare/util/cli/ApplicationHost.java @@ -25,6 +25,7 @@ import org.commcare.util.screen.CommCareSessionException; import org.commcare.util.screen.EntityListSubscreen; import org.commcare.util.screen.EntityScreen; +import org.commcare.util.screen.EntityScreenContext; import org.commcare.util.screen.MenuScreen; import org.commcare.util.screen.MultiSelectEntityScreen; import org.commcare.util.screen.QueryScreen; @@ -465,7 +466,8 @@ private Screen getNextScreen() { return getNextScreen(); } else if (next.equals(SessionFrame.STATE_MULTIPLE_DATUM_VAL)) { try { - return new MultiSelectEntityScreen(true, true, mSession, virtualInstanceStorage, false); + return new MultiSelectEntityScreen(true, true, mSession, virtualInstanceStorage, false, + new EntityScreenContext()); } catch (CommCareSessionException ccse) { printErrorAndContinue("Error during session execution:", ccse); } diff --git a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java index 3986468107..55fb4fb331 100755 --- a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java @@ -3,6 +3,9 @@ import static org.commcare.util.screen.MultiSelectEntityScreen.USE_SELECTED_VALUES; import org.commcare.cases.entity.Entity; +import org.commcare.cases.entity.EntitySortNotificationInterface; +import org.commcare.cases.entity.EntitySorter; +import org.commcare.cases.entity.EntityStringFilterer; import org.commcare.cases.entity.NodeEntityFactory; import org.commcare.modern.util.Pair; import org.commcare.suite.model.Action; @@ -33,6 +36,7 @@ public class EntityListSubscreen extends Subscreen { private static final int SCREEN_WIDTH = 100; private final TreeReference[] entitiesRefs; + private final EntityScreenContext entityScreenContext; private String[] rows; private final String mHeader; @@ -45,24 +49,64 @@ public class EntityListSubscreen extends Subscreen { private List> entities; public EntityListSubscreen(Detail shortDetail, Vector references, EvaluationContext context, - boolean handleCaseIndex) throws CommCareSessionException { + boolean handleCaseIndex, EntityScreenContext entityScreenContext) throws CommCareSessionException { mHeader = createHeader(shortDetail, context); this.shortDetail = shortDetail; this.rootContext = context; this.entitiesRefs = new TreeReference[references.size()]; this.handleCaseIndex = handleCaseIndex; + this.entityScreenContext = entityScreenContext; references.copyInto(entitiesRefs); actions = shortDetail.getCustomActions(context); - initEntities(context, shortDetail); + initEntities(context, shortDetail, entityScreenContext); } - private void initEntities(EvaluationContext context, Detail shortDetail) { + private void initEntities(EvaluationContext context, Detail shortDetail, + EntityScreenContext entityScreenContext) { NodeEntityFactory nodeEntityFactory = new NodeEntityFactory(shortDetail, context); entities = new ArrayList<>(); for (TreeReference reference : entitiesRefs) { entities.add(nodeEntityFactory.getEntity(reference)); } nodeEntityFactory.prepareEntities(entities); + filterEntities(entityScreenContext); + sortEntities(entityScreenContext); + } + + private void filterEntities(EntityScreenContext entityScreenContext) { + String searchText = entityScreenContext.getSearchText(); + boolean isFuzzySearchEnabled = entityScreenContext.isFuzzySearch(); + if (searchText != null && !"".equals(searchText)) { + NodeEntityFactory nodeEntityFactory = new NodeEntityFactory(shortDetail, rootContext); + EntityStringFilterer filterer = new EntityStringFilterer(searchText.split(" "), + nodeEntityFactory, entities, isFuzzySearchEnabled); + entities = filterer.buildMatchList(); + } + } + + private void sortEntities(EntityScreenContext entityScreenContext) { + int sortIndex = entityScreenContext.getSortIndex(); + int[] order; + boolean reverse = false; + if (sortIndex != 0) { + if (sortIndex < 0) { + reverse = true; + sortIndex = Math.abs(sortIndex); + } + // sort index is one indexed so adjust for that + int sortFieldIndex = sortIndex - 1; + order = new int[]{sortFieldIndex}; + } else { + order = shortDetail.getOrderedFieldIndicesForSorting(); + for (int i = 0; i < shortDetail.getFields().length; ++i) { + String header = shortDetail.getFields()[i].getHeader().evaluate(); + if (order.length == 0 && !"".equals(header)) { + order = new int[]{i}; + } + } + } + java.util.Collections.sort(entities, + new EntitySorter(shortDetail.getFields(), reverse, order, new LogNotifier())); } private String[] getRows(Detail detail) { @@ -260,4 +304,11 @@ public Vector getActions() { public List> getEntities() { return entities; } + + private static class LogNotifier implements EntitySortNotificationInterface { + @Override + public void notifyBadFilter(String[] args) { + + } + } } diff --git a/src/cli/java/org/commcare/util/screen/EntityScreen.java b/src/cli/java/org/commcare/util/screen/EntityScreen.java index 993326f5db..39ea355903 100755 --- a/src/cli/java/org/commcare/util/screen/EntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreen.java @@ -37,6 +37,7 @@ */ public class EntityScreen extends CompoundScreenHost { + private final EntityScreenContext entityScreenContext; private TreeReference mCurrentSelection; private SessionWrapper mSession; @@ -67,6 +68,7 @@ public class EntityScreen extends CompoundScreenHost { public EntityScreen(boolean handleCaseIndex) { this.handleCaseIndex = handleCaseIndex; + entityScreenContext = new EntityScreenContext(); } /** @@ -80,15 +82,17 @@ public EntityScreen(boolean handleCaseIndex) { public EntityScreen(boolean handleCaseIndex, boolean needsFullInit) { this.handleCaseIndex = handleCaseIndex; this.needsFullInit = needsFullInit; + entityScreenContext = new EntityScreenContext(); } public EntityScreen(boolean handleCaseIndex, boolean needsFullInit, SessionWrapper session, - boolean isDetailScreen) + boolean isDetailScreen, EntityScreenContext entityScreenContext) throws CommCareSessionException { this.handleCaseIndex = handleCaseIndex; this.needsFullInit = needsFullInit; this.setSession(session); this.isDetailScreen = isDetailScreen; + this.entityScreenContext = entityScreenContext; } public void evaluateAutoLaunch(String nextInput) throws CommCareSessionException { @@ -97,7 +101,7 @@ public void evaluateAutoLaunch(String nextInput) throws CommCareSessionException if (action.isAutoLaunchAction(subContext)) { // Supply an empty case list so we can "select" from it later using getEntityFromID mCurrentScreen = new EntityListSubscreen(mShortDetail, new Vector<>(), evalContext, - handleCaseIndex); + handleCaseIndex, entityScreenContext); this.autoLaunchAction = action; } } @@ -158,7 +162,7 @@ public void init(SessionWrapper session) throws CommCareSessionException { Vector entityListReferences = !needsFullInit || isDetailScreen ? new Vector<>() : references; mCurrentScreen = new EntityListSubscreen(mShortDetail, entityListReferences, evalContext, - handleCaseIndex); + handleCaseIndex, entityScreenContext); } initialized = true; } @@ -348,7 +352,7 @@ public void setCurrentScreenToDetail(int index) throws CommCareSessionException if (detailNodeset != null) { TreeReference contextualizedNodeset = detailNodeset.contextualize(this.mCurrentSelection); this.mCurrentScreen = new EntityListSubscreen(longDetailList[index], - subContext.expandReference(contextualizedNodeset), subContext, handleCaseIndex); + subContext.expandReference(contextualizedNodeset), subContext, handleCaseIndex, entityScreenContext); } else { this.mCurrentScreen = new EntityDetailSubscreen(index, longDetailList[index], subContext, getDetailListTitles(subContext, this.mCurrentSelection)); @@ -483,4 +487,8 @@ public QueryScreen getQueryScreen() { public void setQueryScreen(QueryScreen queryScreen) { this.queryScreen = queryScreen; } + + public EntityScreenContext getEntityScreenContext() { + return entityScreenContext; + } } diff --git a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java index c28a50e34d..a20721d810 100644 --- a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java @@ -39,9 +39,10 @@ public class MultiSelectEntityScreen extends EntityScreen { public MultiSelectEntityScreen(boolean handleCaseIndex, boolean needsFullInit, SessionWrapper session, VirtualDataInstanceStorage virtualDataInstanceStorage, - boolean isDetailScreen) + boolean isDetailScreen, + EntityScreenContext entityScreenContext) throws CommCareSessionException { - super(handleCaseIndex, needsFullInit, session, isDetailScreen); + super(handleCaseIndex, needsFullInit, session, isDetailScreen, entityScreenContext); this.virtualDataInstanceStorage = virtualDataInstanceStorage; } From b16b58ee54361cb218869cedb22bb6d21b94f45a Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 22 May 2023 10:37:35 -0400 Subject: [PATCH 124/402] fix CLI by lazy init rows --- .../java/org/commcare/util/screen/EntityListSubscreen.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java index 55fb4fb331..1ef40edaf7 100755 --- a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java @@ -198,6 +198,7 @@ public void prompt(PrintStream out) { out.println(ScreenUtils.pad("", maxLength + 1) + mHeader); out.println("==========================================================================================="); + initRows(); for (int i = 0; i < entitiesRefs.length; ++i) { String d = rows[i]; out.println(ScreenUtils.pad(String.valueOf(i), maxLength) + ") " + d); @@ -215,10 +216,14 @@ public void prompt(PrintStream out) { @Override public String[] getOptions() { + initRows(); + return rows; + } + + private void initRows() { if (rows == null) { rows = getRows(shortDetail); } - return rows; } @Override From 4a05b8e3b3a2aa92ad82ecb54f71cd0a21a02254 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 22 May 2023 10:38:01 -0400 Subject: [PATCH 125/402] we are sorting in CLI now, fix the order of entities in test --- src/test/java/org/cli/CliTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/cli/CliTests.java b/src/test/java/org/cli/CliTests.java index 2fe8a4d7d2..d4934844a3 100644 --- a/src/test/java/org/cli/CliTests.java +++ b/src/test/java/org/cli/CliTests.java @@ -148,8 +148,8 @@ public void testMultiSelectCaseList() throws Exception { Assert.assertTrue(output.contains("0) Name")); break; case 2: - Assert.assertTrue(output.contains("0) Lucy")); - Assert.assertTrue(output.contains("1) Jack")); + Assert.assertTrue(output.contains("0) Jack")); + Assert.assertTrue(output.contains("1) Lucy")); break; case 3: Assert.assertTrue(output.contains("0) multi-select form with auto-launch case list")); From 5be4eca65613b023a01edf56677c74cb40f58e92 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 22 May 2023 11:55:38 -0400 Subject: [PATCH 126/402] use same instance of nodeEntityFactory to filter --- .../org/commcare/util/screen/EntityListSubscreen.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java index 1ef40edaf7..afc27b9d56 100755 --- a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java @@ -14,11 +14,8 @@ import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.model.trace.AccumulatingReporter; -import org.javarosa.core.model.trace.EvaluationTraceReporter; import org.javarosa.core.model.trace.ReducingTraceReporter; -import org.javarosa.core.model.utils.InstrumentationUtils; import org.javarosa.core.util.DataUtil; -import org.javarosa.xpath.XPathException; import java.io.PrintStream; import java.util.ArrayList; @@ -69,15 +66,14 @@ private void initEntities(EvaluationContext context, Detail shortDetail, entities.add(nodeEntityFactory.getEntity(reference)); } nodeEntityFactory.prepareEntities(entities); - filterEntities(entityScreenContext); + filterEntities(entityScreenContext, nodeEntityFactory); sortEntities(entityScreenContext); } - private void filterEntities(EntityScreenContext entityScreenContext) { + private void filterEntities(EntityScreenContext entityScreenContext, NodeEntityFactory nodeEntityFactory) { String searchText = entityScreenContext.getSearchText(); boolean isFuzzySearchEnabled = entityScreenContext.isFuzzySearch(); if (searchText != null && !"".equals(searchText)) { - NodeEntityFactory nodeEntityFactory = new NodeEntityFactory(shortDetail, rootContext); EntityStringFilterer filterer = new EntityStringFilterer(searchText.split(" "), nodeEntityFactory, entities, isFuzzySearchEnabled); entities = filterer.buildMatchList(); From 23064e61ac8990575d806df604ea1445bc56ff8c Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 22 May 2023 11:56:05 -0400 Subject: [PATCH 127/402] set casePerPage to default in case 0 --- src/cli/java/org/commcare/util/screen/EntityScreenContext.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityScreenContext.java b/src/cli/java/org/commcare/util/screen/EntityScreenContext.java index 48ac88b2bd..c6b50b99e8 100644 --- a/src/cli/java/org/commcare/util/screen/EntityScreenContext.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreenContext.java @@ -24,7 +24,7 @@ public EntityScreenContext(int offset, String searchText, int sortIndex, int cas mOffSet = offset; mSearchText = searchText; mSortIndex = sortIndex; - mCasesPerPage = casesPerPage; + mCasesPerPage = casesPerPage == 0 ? DEFAULT_CASES_PER_PAGE : casesPerPage; mSelectedValues = selectedValues; mDetailSelection = detailSelection; fuzzySearch = isFuzzySearch; From b6bec70873b7a2749ccffaa8f3f1d75e4aee070f Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 22 May 2023 15:58:23 -0400 Subject: [PATCH 128/402] move initEntities to a helper class to be able to utilise it elsewhere --- .../util/screen/EntityListSubscreen.java | 66 +------------- .../util/screen/EntityScreenHelper.java | 85 +++++++++++++++++++ 2 files changed, 88 insertions(+), 63 deletions(-) create mode 100644 src/cli/java/org/commcare/util/screen/EntityScreenHelper.java diff --git a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java index afc27b9d56..4f83171c5b 100755 --- a/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityListSubscreen.java @@ -1,12 +1,9 @@ package org.commcare.util.screen; +import static org.commcare.util.screen.EntityScreenHelper.initEntities; import static org.commcare.util.screen.MultiSelectEntityScreen.USE_SELECTED_VALUES; import org.commcare.cases.entity.Entity; -import org.commcare.cases.entity.EntitySortNotificationInterface; -import org.commcare.cases.entity.EntitySorter; -import org.commcare.cases.entity.EntityStringFilterer; -import org.commcare.cases.entity.NodeEntityFactory; import org.commcare.modern.util.Pair; import org.commcare.suite.model.Action; import org.commcare.suite.model.Detail; @@ -18,7 +15,6 @@ import org.javarosa.core.util.DataUtil; import java.io.PrintStream; -import java.util.ArrayList; import java.util.List; import java.util.Vector; @@ -33,7 +29,6 @@ public class EntityListSubscreen extends Subscreen { private static final int SCREEN_WIDTH = 100; private final TreeReference[] entitiesRefs; - private final EntityScreenContext entityScreenContext; private String[] rows; private final String mHeader; @@ -50,59 +45,11 @@ public EntityListSubscreen(Detail shortDetail, Vector references, mHeader = createHeader(shortDetail, context); this.shortDetail = shortDetail; this.rootContext = context; - this.entitiesRefs = new TreeReference[references.size()]; this.handleCaseIndex = handleCaseIndex; - this.entityScreenContext = entityScreenContext; + this.entitiesRefs = new TreeReference[references.size()]; references.copyInto(entitiesRefs); actions = shortDetail.getCustomActions(context); - initEntities(context, shortDetail, entityScreenContext); - } - - private void initEntities(EvaluationContext context, Detail shortDetail, - EntityScreenContext entityScreenContext) { - NodeEntityFactory nodeEntityFactory = new NodeEntityFactory(shortDetail, context); - entities = new ArrayList<>(); - for (TreeReference reference : entitiesRefs) { - entities.add(nodeEntityFactory.getEntity(reference)); - } - nodeEntityFactory.prepareEntities(entities); - filterEntities(entityScreenContext, nodeEntityFactory); - sortEntities(entityScreenContext); - } - - private void filterEntities(EntityScreenContext entityScreenContext, NodeEntityFactory nodeEntityFactory) { - String searchText = entityScreenContext.getSearchText(); - boolean isFuzzySearchEnabled = entityScreenContext.isFuzzySearch(); - if (searchText != null && !"".equals(searchText)) { - EntityStringFilterer filterer = new EntityStringFilterer(searchText.split(" "), - nodeEntityFactory, entities, isFuzzySearchEnabled); - entities = filterer.buildMatchList(); - } - } - - private void sortEntities(EntityScreenContext entityScreenContext) { - int sortIndex = entityScreenContext.getSortIndex(); - int[] order; - boolean reverse = false; - if (sortIndex != 0) { - if (sortIndex < 0) { - reverse = true; - sortIndex = Math.abs(sortIndex); - } - // sort index is one indexed so adjust for that - int sortFieldIndex = sortIndex - 1; - order = new int[]{sortFieldIndex}; - } else { - order = shortDetail.getOrderedFieldIndicesForSorting(); - for (int i = 0; i < shortDetail.getFields().length; ++i) { - String header = shortDetail.getFields()[i].getHeader().evaluate(); - if (order.length == 0 && !"".equals(header)) { - order = new int[]{i}; - } - } - } - java.util.Collections.sort(entities, - new EntitySorter(shortDetail.getFields(), reverse, order, new LogNotifier())); + entities = initEntities(context, shortDetail, entityScreenContext, entitiesRefs); } private String[] getRows(Detail detail) { @@ -305,11 +252,4 @@ public Vector getActions() { public List> getEntities() { return entities; } - - private static class LogNotifier implements EntitySortNotificationInterface { - @Override - public void notifyBadFilter(String[] args) { - - } - } } diff --git a/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java b/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java new file mode 100644 index 0000000000..1ea33f29e0 --- /dev/null +++ b/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java @@ -0,0 +1,85 @@ +package org.commcare.util.screen; + +import org.commcare.cases.entity.Entity; +import org.commcare.cases.entity.EntitySortNotificationInterface; +import org.commcare.cases.entity.EntitySorter; +import org.commcare.cases.entity.EntityStringFilterer; +import org.commcare.cases.entity.NodeEntityFactory; +import org.commcare.suite.model.Detail; +import org.javarosa.core.model.condition.EvaluationContext; +import org.javarosa.core.model.instance.TreeReference; + +import java.util.ArrayList; +import java.util.List; + +/** + * Common methods for initialising entities + */ +public class EntityScreenHelper { + + /** + * Initialises given entity references into Entity models + * @param context evaluation context to calculate detail fields + * @param detail detail definition to map the given entity references to + * @param entityScreenContext entity screen context + * @param entitiesRefs references to initialise + * @return List of initialised entity models + */ + public static List> initEntities(EvaluationContext context, Detail detail, + EntityScreenContext entityScreenContext, TreeReference[] entitiesRefs) { + NodeEntityFactory nodeEntityFactory = new NodeEntityFactory(detail, context); + List> entities = new ArrayList<>(); + for (TreeReference reference : entitiesRefs) { + entities.add(nodeEntityFactory.getEntity(reference)); + } + nodeEntityFactory.prepareEntities(entities); + entities = filterEntities(entityScreenContext, nodeEntityFactory, entities); + return sortEntities(entityScreenContext, entities, detail); + } + + private static List> filterEntities(EntityScreenContext entityScreenContext, NodeEntityFactory nodeEntityFactory, + List> entities) { + String searchText = entityScreenContext.getSearchText(); + boolean isFuzzySearchEnabled = entityScreenContext.isFuzzySearch(); + if (searchText != null && !"".equals(searchText)) { + EntityStringFilterer filterer = new EntityStringFilterer(searchText.split(" "), + nodeEntityFactory, entities, isFuzzySearchEnabled); + entities = filterer.buildMatchList(); + } + return entities; + } + + private static List> sortEntities(EntityScreenContext entityScreenContext, List> entities, + Detail shortDetail) { + int sortIndex = entityScreenContext.getSortIndex(); + int[] order; + boolean reverse = false; + if (sortIndex != 0) { + if (sortIndex < 0) { + reverse = true; + sortIndex = Math.abs(sortIndex); + } + // sort index is one indexed so adjust for that + int sortFieldIndex = sortIndex - 1; + order = new int[]{sortFieldIndex}; + } else { + order = shortDetail.getOrderedFieldIndicesForSorting(); + for (int i = 0; i < shortDetail.getFields().length; ++i) { + String header = shortDetail.getFields()[i].getHeader().evaluate(); + if (order.length == 0 && !"".equals(header)) { + order = new int[]{i}; + } + } + } + java.util.Collections.sort(entities, + new EntitySorter(shortDetail.getFields(), reverse, order, new LogNotifier())); + return entities; + } + + private static class LogNotifier implements EntitySortNotificationInterface { + @Override + public void notifyBadFilter(String[] args) { + + } + } +} From 3768531f98d6e2b33fb3233780e542ba4ad782fd Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 22 May 2023 16:25:03 -0400 Subject: [PATCH 129/402] get isDetailScreen from entity screen context --- .../java/org/commcare/util/cli/ApplicationHost.java | 2 +- .../java/org/commcare/util/screen/EntityScreen.java | 12 +++++++----- .../util/screen/MultiSelectEntityScreen.java | 3 +-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/cli/java/org/commcare/util/cli/ApplicationHost.java b/src/cli/java/org/commcare/util/cli/ApplicationHost.java index f8367ff96b..01ffc175a4 100644 --- a/src/cli/java/org/commcare/util/cli/ApplicationHost.java +++ b/src/cli/java/org/commcare/util/cli/ApplicationHost.java @@ -466,7 +466,7 @@ private Screen getNextScreen() { return getNextScreen(); } else if (next.equals(SessionFrame.STATE_MULTIPLE_DATUM_VAL)) { try { - return new MultiSelectEntityScreen(true, true, mSession, virtualInstanceStorage, false, + return new MultiSelectEntityScreen(true, true, mSession, virtualInstanceStorage, new EntityScreenContext()); } catch (CommCareSessionException ccse) { printErrorAndContinue("Error during session execution:", ccse); diff --git a/src/cli/java/org/commcare/util/screen/EntityScreen.java b/src/cli/java/org/commcare/util/screen/EntityScreen.java index 39ea355903..9e4b9ac378 100755 --- a/src/cli/java/org/commcare/util/screen/EntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreen.java @@ -57,7 +57,6 @@ public class EntityScreen extends CompoundScreenHost { private boolean handleCaseIndex; private boolean needsFullInit = true; - private boolean isDetailScreen = false; protected Vector references; @@ -86,12 +85,11 @@ public EntityScreen(boolean handleCaseIndex, boolean needsFullInit) { } public EntityScreen(boolean handleCaseIndex, boolean needsFullInit, SessionWrapper session, - boolean isDetailScreen, EntityScreenContext entityScreenContext) + EntityScreenContext entityScreenContext) throws CommCareSessionException { this.handleCaseIndex = handleCaseIndex; this.needsFullInit = needsFullInit; this.setSession(session); - this.isDetailScreen = isDetailScreen; this.entityScreenContext = entityScreenContext; } @@ -160,7 +158,7 @@ public void init(SessionWrapper session) throws CommCareSessionException { // if isDetailScreen or needsFullInit is not set, // sub screen is needed to handle actions but we can skip eval refs Vector entityListReferences = - !needsFullInit || isDetailScreen ? new Vector<>() : references; + !needsFullInit || isDetailScreen() ? new Vector<>() : references; mCurrentScreen = new EntityListSubscreen(mShortDetail, entityListReferences, evalContext, handleCaseIndex, entityScreenContext); } @@ -304,12 +302,16 @@ public void updateSelection(String input, TreeReference[] selectedRefs) throws C } private void showDetailScreen() throws CommCareSessionException { - if (isDetailScreen) { + if (isDetailScreen()) { // Set entity screen to show detail and redraw setCurrentScreenToDetail(); } } + private boolean isDetailScreen() { + return entityScreenContext.getDetailSelection() != null; + } + @Trace public void setSelectedEntity(TreeReference selection) { this.mCurrentSelection = selection; diff --git a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java index a20721d810..e7b774ab90 100644 --- a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java @@ -39,10 +39,9 @@ public class MultiSelectEntityScreen extends EntityScreen { public MultiSelectEntityScreen(boolean handleCaseIndex, boolean needsFullInit, SessionWrapper session, VirtualDataInstanceStorage virtualDataInstanceStorage, - boolean isDetailScreen, EntityScreenContext entityScreenContext) throws CommCareSessionException { - super(handleCaseIndex, needsFullInit, session, isDetailScreen, entityScreenContext); + super(handleCaseIndex, needsFullInit, session, entityScreenContext); this.virtualDataInstanceStorage = virtualDataInstanceStorage; } From 08b839fb09ed5ce226007ae73b683a11a02e4480 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 24 May 2023 09:31:51 -0400 Subject: [PATCH 130/402] better way to eval order, should be no-op --- .../org/commcare/util/screen/EntityScreenHelper.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java b/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java index 1ea33f29e0..f7da5aed0a 100644 --- a/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java @@ -64,10 +64,13 @@ private static List> sortEntities(EntityScreenContext enti order = new int[]{sortFieldIndex}; } else { order = shortDetail.getOrderedFieldIndicesForSorting(); - for (int i = 0; i < shortDetail.getFields().length; ++i) { - String header = shortDetail.getFields()[i].getHeader().evaluate(); - if (order.length == 0 && !"".equals(header)) { - order = new int[]{i}; + if (order.length == 0) { + for (int i = 0; i < shortDetail.getFields().length; ++i) { + String header = shortDetail.getFields()[i].getHeader().evaluate(); + if (!"".equals(header)) { + order = new int[]{i}; + break; + } } } } From 0003193f0c13fd46fc69c81fc5430b08fefc603a Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 29 May 2023 07:01:26 -0400 Subject: [PATCH 131/402] lint --- .../java/org/commcare/util/screen/EntityScreen.java | 3 ++- .../commcare/util/screen/EntityScreenHelper.java | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityScreen.java b/src/cli/java/org/commcare/util/screen/EntityScreen.java index 9e4b9ac378..bd03d8b82b 100755 --- a/src/cli/java/org/commcare/util/screen/EntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreen.java @@ -354,7 +354,8 @@ public void setCurrentScreenToDetail(int index) throws CommCareSessionException if (detailNodeset != null) { TreeReference contextualizedNodeset = detailNodeset.contextualize(this.mCurrentSelection); this.mCurrentScreen = new EntityListSubscreen(longDetailList[index], - subContext.expandReference(contextualizedNodeset), subContext, handleCaseIndex, entityScreenContext); + subContext.expandReference(contextualizedNodeset), subContext, handleCaseIndex, + entityScreenContext); } else { this.mCurrentScreen = new EntityDetailSubscreen(index, longDetailList[index], subContext, getDetailListTitles(subContext, this.mCurrentSelection)); diff --git a/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java b/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java index f7da5aed0a..e4cd1f19a9 100644 --- a/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java @@ -19,10 +19,11 @@ public class EntityScreenHelper { /** * Initialises given entity references into Entity models - * @param context evaluation context to calculate detail fields - * @param detail detail definition to map the given entity references to + * + * @param context evaluation context to calculate detail fields + * @param detail detail definition to map the given entity references to * @param entityScreenContext entity screen context - * @param entitiesRefs references to initialise + * @param entitiesRefs references to initialise * @return List of initialised entity models */ public static List> initEntities(EvaluationContext context, Detail detail, @@ -37,7 +38,8 @@ public static List> initEntities(EvaluationContext context return sortEntities(entityScreenContext, entities, detail); } - private static List> filterEntities(EntityScreenContext entityScreenContext, NodeEntityFactory nodeEntityFactory, + private static List> filterEntities(EntityScreenContext entityScreenContext, + NodeEntityFactory nodeEntityFactory, List> entities) { String searchText = entityScreenContext.getSearchText(); boolean isFuzzySearchEnabled = entityScreenContext.isFuzzySearch(); @@ -49,7 +51,8 @@ private static List> filterEntities(EntityScreenContext en return entities; } - private static List> sortEntities(EntityScreenContext entityScreenContext, List> entities, + private static List> sortEntities(EntityScreenContext entityScreenContext, + List> entities, Detail shortDetail) { int sortIndex = entityScreenContext.getSortIndex(); int[] order; From fd9d3b499d74a353a9ca72d95de1814bb33c04a1 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 6 Apr 2023 15:04:12 -0300 Subject: [PATCH 132/402] adds keys to get choices --- .../java/org/javarosa/core/model/utils/ItemSetUtils.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/utils/ItemSetUtils.java b/src/main/java/org/javarosa/core/model/utils/ItemSetUtils.java index 4d85f45870..f90ccce86c 100644 --- a/src/main/java/org/javarosa/core/model/utils/ItemSetUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/ItemSetUtils.java @@ -2,6 +2,7 @@ import org.commcare.cases.query.QueryContext; import org.commcare.cases.query.ScopeLimitedReferenceRequestCache; +import org.commcare.modern.util.Pair; import org.javarosa.core.model.ItemsetBinding; import org.javarosa.core.model.SelectChoice; import org.javarosa.core.model.condition.EvaluationContext; @@ -214,13 +215,15 @@ public static int getIndexOf(ItemsetBinding itemsetBinding, String value) { } // returns labels corresponding to choices associated with the given itemsetBinding - public static String[] getChoiceLabels(ItemsetBinding itemsetBinding) { + public static Pair getChoices(ItemsetBinding itemsetBinding) { Vector selectChoices = itemsetBinding.getChoices(); String[] choiceLabels = new String[selectChoices.size()]; + String[] choiceKeys = new String[selectChoices.size()]; for (int i = 0; i < selectChoices.size(); i++) { SelectChoice selectChoice = selectChoices.get(i); choiceLabels[i] = selectChoice.getLabelInnerText(); + choiceKeys[i] = selectChoice.getValue(); } - return choiceLabels; + return new Pair(choiceKeys, choiceLabels); } } From de3d5a6cfa9e6a3676dff09a35b920b1613b4671 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 6 Apr 2023 15:04:21 -0300 Subject: [PATCH 133/402] skips index to keys conversion if selectValuesByKey is set --- .../org/commcare/util/screen/QueryScreen.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/QueryScreen.java b/src/cli/java/org/commcare/util/screen/QueryScreen.java index 877c691cfe..f8615541d5 100644 --- a/src/cli/java/org/commcare/util/screen/QueryScreen.java +++ b/src/cli/java/org/commcare/util/screen/QueryScreen.java @@ -179,8 +179,7 @@ private String getInstanceKey(String instanceId, Map values) { } return CryptUtil.sha256(builder.toString()); } - - public void answerPrompts(Hashtable answers) { + public void answerPrompts(Hashtable answers, boolean selectValuesByKeys) { for (Enumeration en = userInputDisplays.keys(); en.hasMoreElements(); ) { String key = (String)en.nextElement(); QueryPrompt queryPrompt = userInputDisplays.get(key); @@ -193,17 +192,19 @@ public void answerPrompts(Hashtable answers) { Vector selectChoices = queryPrompt.getItemsetBinding().getChoices(); String[] indicesOfSelectedChoices = RemoteQuerySessionManager.extractMultipleChoices(answer); ArrayList selectedChoices = new ArrayList<>(indicesOfSelectedChoices.length); - for (int i = 0; i < indicesOfSelectedChoices.length; i++) { - if (indicesOfSelectedChoices[i].isEmpty()) { - selectedChoices.add(""); - } else { - int choiceIndex = Integer.parseInt(indicesOfSelectedChoices[i]); - if (choiceIndex < selectChoices.size() && choiceIndex > -1) { - selectedChoices.add(selectChoices.get(choiceIndex).getValue()); + if (!selectValuesByKeys) { + for (int i = 0; i < indicesOfSelectedChoices.length; i++) { + if (indicesOfSelectedChoices[i].isEmpty()) { + selectedChoices.add(""); + } else { + int choiceIndex = Integer.parseInt(indicesOfSelectedChoices[i]); + if (choiceIndex < selectChoices.size() && choiceIndex > -1) { + selectedChoices.add(selectChoices.get(choiceIndex).getValue()); + } } } + answer = String.join(RemoteQuerySessionManager.ANSWER_DELIMITER, selectedChoices); } - answer = String.join(RemoteQuerySessionManager.ANSWER_DELIMITER, selectedChoices); } remoteQuerySessionManager.answerUserPrompt(key, answer); } @@ -262,7 +263,7 @@ public boolean handleInputAndUpdateSession(CommCareSession session, String input userAnswers.put(queryPromptEntry.getKey(), answers[count]); count++; } - answerPrompts(userAnswers); + answerPrompts(userAnswers, true); URL url = getBaseUrl(); Multimap requestData = getQueryParams(false); InputStream response = sessionUtils.makeQueryRequest(url, requestData, domainedUsername, password); From e8f5c68fa728636ab7ba752e98e726aa2ee5d5ef Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 18 Apr 2023 12:00:37 -0300 Subject: [PATCH 134/402] expand if scope to include code that only gets used in the if block --- src/cli/java/org/commcare/util/screen/QueryScreen.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/QueryScreen.java b/src/cli/java/org/commcare/util/screen/QueryScreen.java index f8615541d5..02e4995ca6 100644 --- a/src/cli/java/org/commcare/util/screen/QueryScreen.java +++ b/src/cli/java/org/commcare/util/screen/QueryScreen.java @@ -189,10 +189,10 @@ public void answerPrompts(Hashtable answers, boolean selectValue // be converted to the corresponding value if (queryPrompt.isSelect() && !StringUtils.isEmpty(answer)) { remoteQuerySessionManager.populateItemSetChoices(queryPrompt); - Vector selectChoices = queryPrompt.getItemsetBinding().getChoices(); - String[] indicesOfSelectedChoices = RemoteQuerySessionManager.extractMultipleChoices(answer); - ArrayList selectedChoices = new ArrayList<>(indicesOfSelectedChoices.length); if (!selectValuesByKeys) { + Vector selectChoices = queryPrompt.getItemsetBinding().getChoices(); + String[] indicesOfSelectedChoices = RemoteQuerySessionManager.extractMultipleChoices(answer); + ArrayList selectedChoices = new ArrayList<>(indicesOfSelectedChoices.length); for (int i = 0; i < indicesOfSelectedChoices.length; i++) { if (indicesOfSelectedChoices[i].isEmpty()) { selectedChoices.add(""); From e1fecbdd91840bcd392dd09235318283eefd0985 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 15 May 2023 10:50:01 -0400 Subject: [PATCH 135/402] stickler --- src/cli/java/org/commcare/util/screen/QueryScreen.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/java/org/commcare/util/screen/QueryScreen.java b/src/cli/java/org/commcare/util/screen/QueryScreen.java index 02e4995ca6..7eee1ee2e2 100644 --- a/src/cli/java/org/commcare/util/screen/QueryScreen.java +++ b/src/cli/java/org/commcare/util/screen/QueryScreen.java @@ -120,7 +120,7 @@ private String getTitleLocaleStringLegacy() { } catch (NoLocalizedTextException | NullPointerException e) { mTitle = "Case Claim"; } - return mTitle; + return mTitle; } // Formplayer List of Supported prompts @@ -134,6 +134,7 @@ private ArrayList getSupportedPrompts() { supportedPrompts.add(INPUT_TYPE_ADDRESS); return supportedPrompts; } + public Pair processResponse(InputStream responseData, URL url, Multimap requestData) { if (responseData == null) { @@ -179,6 +180,7 @@ private String getInstanceKey(String instanceId, Map values) { } return CryptUtil.sha256(builder.toString()); } + public void answerPrompts(Hashtable answers, boolean selectValuesByKeys) { for (Enumeration en = userInputDisplays.keys(); en.hasMoreElements(); ) { String key = (String)en.nextElement(); From 8ba2e9b950f260882c9e0e661627cd0a20adebd2 Mon Sep 17 00:00:00 2001 From: Dave Viggiano Date: Thu, 8 Jun 2023 11:32:59 -0400 Subject: [PATCH 136/402] Using a flag with ProvidedAuth to indicate when to wrap username with domain (instead of additional BasicAuth class). --- src/main/java/org/commcare/core/network/AuthInfo.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/commcare/core/network/AuthInfo.java b/src/main/java/org/commcare/core/network/AuthInfo.java index 26a65906a1..7e874eb40e 100644 --- a/src/main/java/org/commcare/core/network/AuthInfo.java +++ b/src/main/java/org/commcare/core/network/AuthInfo.java @@ -8,24 +8,23 @@ public abstract class AuthInfo { public String username; public String password; + public boolean wrapDomain; public static class NoAuth extends AuthInfo { } public static class ProvidedAuth extends AuthInfo { - public ProvidedAuth(String username, String password) { - this.username = username; - this.password = password; + this(username, password, true); } - } - public static class BasicAuth extends AuthInfo { - public BasicAuth(String username, String password) { + public ProvidedAuth(String username, String password, boolean wrapDomain) { this.username = username; this.password = password; + this.wrapDomain = wrapDomain; } + } // Auth with the currently-logged in user From f0f13436599e094327723d8930a76adcb02cfe97 Mon Sep 17 00:00:00 2001 From: Jennifer Schweers Date: Thu, 8 Jun 2023 18:06:03 -0400 Subject: [PATCH 137/402] Updated Style to acknowledge image-enum fields as images --- src/main/java/org/commcare/suite/model/Style.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/commcare/suite/model/Style.java b/src/main/java/org/commcare/suite/model/Style.java index 86968bfcfb..fd3edebf4b 100644 --- a/src/main/java/org/commcare/suite/model/Style.java +++ b/src/main/java/org/commcare/suite/model/Style.java @@ -69,6 +69,7 @@ private void setWidthHint(int widthHint) { private void setDisplayFormatFromString(String displayFormat){ switch (displayFormat) { case "image": + case "enum-image": setDisplayFormat(DisplayFormat.Image); break; case "audio": From 29c58eca609ad514399cfee00ef94553a2ff1820 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Wed, 21 Jun 2023 13:21:08 -0500 Subject: [PATCH 138/402] Add display format AddressPopup --- src/main/java/org/commcare/suite/model/Style.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/commcare/suite/model/Style.java b/src/main/java/org/commcare/suite/model/Style.java index fd3edebf4b..5c910f59dc 100644 --- a/src/main/java/org/commcare/suite/model/Style.java +++ b/src/main/java/org/commcare/suite/model/Style.java @@ -37,6 +37,7 @@ enum DisplayFormat { Audio, Text, Address, + AddressPopup, Graph, Phone, Markdown, @@ -81,6 +82,9 @@ private void setDisplayFormatFromString(String displayFormat){ case "address": setDisplayFormat(DisplayFormat.Address); break; + case "address_popup": + setDisplayFormat(DisplayFormat.AddressPopup); + break; case "graph": setDisplayFormat(DisplayFormat.Graph); break; From 4b093750e68728fef417f28bad826a93b7bdde8b Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Thu, 22 Jun 2023 10:51:02 -0500 Subject: [PATCH 139/402] Use dash instead of underscore for consistency --- src/main/java/org/commcare/suite/model/Style.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/suite/model/Style.java b/src/main/java/org/commcare/suite/model/Style.java index 5c910f59dc..32fa390942 100644 --- a/src/main/java/org/commcare/suite/model/Style.java +++ b/src/main/java/org/commcare/suite/model/Style.java @@ -82,7 +82,7 @@ private void setDisplayFormatFromString(String displayFormat){ case "address": setDisplayFormat(DisplayFormat.Address); break; - case "address_popup": + case "address-popup": setDisplayFormat(DisplayFormat.AddressPopup); break; case "graph": From ee98102919ca552ce6533388526cc8ce19ee1aa9 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 6 Jun 2023 18:43:28 -0400 Subject: [PATCH 140/402] sortEntities doesn't need to return entities explicitly --- .../java/org/commcare/util/screen/EntityScreenHelper.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java b/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java index e4cd1f19a9..878eee0d78 100644 --- a/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java @@ -35,7 +35,8 @@ public static List> initEntities(EvaluationContext context } nodeEntityFactory.prepareEntities(entities); entities = filterEntities(entityScreenContext, nodeEntityFactory, entities); - return sortEntities(entityScreenContext, entities, detail); + sortEntities(entityScreenContext, entities, detail); + return entities; } private static List> filterEntities(EntityScreenContext entityScreenContext, @@ -51,7 +52,7 @@ private static List> filterEntities(EntityScreenContext en return entities; } - private static List> sortEntities(EntityScreenContext entityScreenContext, + private static void sortEntities(EntityScreenContext entityScreenContext, List> entities, Detail shortDetail) { int sortIndex = entityScreenContext.getSortIndex(); @@ -79,7 +80,6 @@ private static List> sortEntities(EntityScreenContext enti } java.util.Collections.sort(entities, new EntitySorter(shortDetail.getFields(), reverse, order, new LogNotifier())); - return entities; } private static class LogNotifier implements EntitySortNotificationInterface { From ba73f8ca739fd38ff3574c5b6a50582f5c72ba53 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 6 Jun 2023 20:04:46 -0400 Subject: [PATCH 141/402] Adds parsing for detail.group node --- .../java/org/commcare/suite/model/Detail.java | 12 ++++- .../org/commcare/suite/model/DetailGroup.java | 51 ++++++++++++++++++ .../org/commcare/xml/DetailGroupParser.java | 52 +++++++++++++++++++ .../java/org/commcare/xml/DetailParser.java | 8 ++- src/test/resources/app_structure/suite.xml | 1 + 5 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/commcare/suite/model/DetailGroup.java create mode 100644 src/main/java/org/commcare/xml/DetailGroupParser.java diff --git a/src/main/java/org/commcare/suite/model/Detail.java b/src/main/java/org/commcare/suite/model/Detail.java index 3c6bd8c1b5..8aec0bc98a 100755 --- a/src/main/java/org/commcare/suite/model/Detail.java +++ b/src/main/java/org/commcare/suite/model/Detail.java @@ -104,6 +104,8 @@ public class Detail implements Externalizable { // equal to its width, rather than being computed independently private boolean useUniformUnitsInCaseTile; + private DetailGroup group; + // ENDREGION /** @@ -117,7 +119,7 @@ public Detail(String id, DisplayUnit title, Text noItemsText, String nodeset, Ve Vector fieldsVector, OrderedHashtable variables, Vector actions, Callout callout, String fitAcross, String uniformUnitsString, String forceLandscape, String focusFunction, - String printPathProvided, String relevancy, Global global) { + String printPathProvided, String relevancy, Global global, DetailGroup group) { if (detailsVector.size() > 0 && fieldsVector.size() > 0) { throw new IllegalArgumentException("A detail may contain either sub-details or fields, but not both."); @@ -166,6 +168,7 @@ public Detail(String id, DisplayUnit title, Text noItemsText, String nodeset, Ve } } this.global = global; + this.group = group; } /** @@ -270,6 +273,7 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOExcep useUniformUnitsInCaseTile = ExtUtil.readBool(in); parsedRelevancyExpression = (XPathExpression)ExtUtil.read(in, new ExtWrapNullable(new ExtWrapTagged()), pf); global = (Global)ExtUtil.read(in, new ExtWrapNullable(new ExtWrapTagged()), pf); + group = (DetailGroup) ExtUtil.read(in, new ExtWrapNullable(DetailGroup.class), pf); } @Override @@ -291,6 +295,7 @@ public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.write(out, new ExtWrapNullable( parsedRelevancyExpression == null ? null : new ExtWrapTagged(parsedRelevancyExpression))); ExtUtil.write(out, new ExtWrapNullable(global == null ? null : new ExtWrapTagged(global))); + ExtUtil.write(out, new ExtWrapNullable(group)); } public OrderedHashtable getVariableDeclarations() { @@ -606,4 +611,9 @@ private boolean isRelevant(EvaluationContext context) { public Global getGlobal() { return global; } + + @Nullable + public DetailGroup getGroup() { + return group; + } } diff --git a/src/main/java/org/commcare/suite/model/DetailGroup.java b/src/main/java/org/commcare/suite/model/DetailGroup.java new file mode 100644 index 0000000000..74e1509b24 --- /dev/null +++ b/src/main/java/org/commcare/suite/model/DetailGroup.java @@ -0,0 +1,51 @@ +package org.commcare.suite.model; + +import org.javarosa.core.util.externalizable.DeserializationException; +import org.javarosa.core.util.externalizable.ExtUtil; +import org.javarosa.core.util.externalizable.ExtWrapTagged; +import org.javarosa.core.util.externalizable.Externalizable; +import org.javarosa.core.util.externalizable.PrototypeFactory; +import org.javarosa.xpath.expr.XPathExpression; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class DetailGroup implements Externalizable { + + private XPathExpression function; + private Integer headerRows; + + /** + * Serialization only!!! + */ + @SuppressWarnings("unused") + public DetailGroup() { + } + + public DetailGroup(XPathExpression function, Integer headerRows) { + this.function = function; + this.headerRows = headerRows; + } + + @Override + public void readExternal(DataInputStream in, PrototypeFactory pf) + throws IOException, DeserializationException { + function = (XPathExpression)ExtUtil.read(in, new ExtWrapTagged(), pf); + headerRows = ExtUtil.readInt(in); + } + + @Override + public void writeExternal(DataOutputStream out) throws IOException { + ExtUtil.write(out, new ExtWrapTagged(function)); + ExtUtil.write(out, headerRows); + } + + public XPathExpression getFunction() { + return function; + } + + public Integer getHeaderRows() { + return headerRows; + } +} diff --git a/src/main/java/org/commcare/xml/DetailGroupParser.java b/src/main/java/org/commcare/xml/DetailGroupParser.java new file mode 100644 index 0000000000..f9a77d9c76 --- /dev/null +++ b/src/main/java/org/commcare/xml/DetailGroupParser.java @@ -0,0 +1,52 @@ +package org.commcare.xml; + +import org.commcare.suite.model.DetailGroup; +import org.javarosa.xml.util.InvalidStructureException; +import org.javarosa.xpath.XPathParseTool; +import org.javarosa.xpath.expr.XPathExpression; +import org.javarosa.xpath.parser.XPathSyntaxException; +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +public class DetailGroupParser extends CommCareElementParser { + + public static final String NAME_GROUP = "group"; + public static final String ATTRIBUTE_NAME_FUNCTION = "function"; + public static final String ATTRIBUTE_NAME_HEADER_ROWS = "header-rows"; + + public DetailGroupParser(KXmlParser parser) { + super(parser); + } + + @Override + public DetailGroup parse() throws InvalidStructureException, IOException, XmlPullParserException { + checkNode(NAME_GROUP); + String functionStr = parser.getAttributeValue(null, ATTRIBUTE_NAME_FUNCTION); + XPathExpression function; + if (functionStr == null) { + throw new InvalidStructureException("No function in detail group declaration " + parser.getName(), + parser); + } + try { + function = XPathParseTool.parseXPath(functionStr); + } catch (XPathSyntaxException e) { + e.printStackTrace(); + throw new InvalidStructureException("Invalid XPath function " + functionStr + ". " + e.getMessage(), + parser); + } + String headerRowsStr = parser.getAttributeValue(null, ATTRIBUTE_NAME_HEADER_ROWS); + if (headerRowsStr == null) { + headerRowsStr = "1"; + } + Integer headerRows; + try { + headerRows = Integer.parseInt(headerRowsStr); + } catch (NumberFormatException e) { + throw new InvalidStructureException( + "non integer value for header-rows " + headerRowsStr + ". " + e.getMessage(), parser); + } + return new DetailGroup(function, headerRows); + } +} diff --git a/src/main/java/org/commcare/xml/DetailParser.java b/src/main/java/org/commcare/xml/DetailParser.java index 9d83e21c9f..f72da3f347 100755 --- a/src/main/java/org/commcare/xml/DetailParser.java +++ b/src/main/java/org/commcare/xml/DetailParser.java @@ -4,6 +4,7 @@ import org.commcare.suite.model.Callout; import org.commcare.suite.model.Detail; import org.commcare.suite.model.DetailField; +import org.commcare.suite.model.DetailGroup; import org.commcare.suite.model.DisplayUnit; import org.commcare.suite.model.Global; import org.commcare.suite.model.Text; @@ -62,6 +63,7 @@ public Detail parse() throws InvalidStructureException, IOException, XmlPullPars Vector fields = new Vector<>(); OrderedHashtable variables = new OrderedHashtable<>(); String focusFunction = null; + DetailGroup detailGroup = null; while (nextTagInBlock("detail")) { if (GlobalParser.NAME_GLOBAL.equals(parser.getName().toLowerCase())) { @@ -115,6 +117,10 @@ public Detail parse() throws InvalidStructureException, IOException, XmlPullPars actions.addElement(new ActionParser(parser).parse()); continue; } + if (DetailGroupParser.NAME_GROUP.equalsIgnoreCase(parser.getName())) { + detailGroup = new DetailGroupParser(parser).parse(); + continue; + } if (parser.getName().equals("detail")) { subdetails.addElement(getDetailParser().parse()); } else { @@ -125,7 +131,7 @@ public Detail parse() throws InvalidStructureException, IOException, XmlPullPars return new Detail(id, title, noItemsText, nodeset, subdetails, fields, variables, actions, callout, fitAcross, useUniformUnits, forceLandscapeView, focusFunction, printTemplatePath, - relevancy, global); + relevancy, global, detailGroup); } protected DetailParser getDetailParser() { diff --git a/src/test/resources/app_structure/suite.xml b/src/test/resources/app_structure/suite.xml index 09f392de1d..9aef2f86e5 100644 --- a/src/test/resources/app_structure/suite.xml +++ b/src/test/resources/app_structure/suite.xml @@ -113,6 +113,7 @@ + From 602dcda831e4656fa46c1b195ceee0b7e9907454 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 6 Jun 2023 20:07:26 -0400 Subject: [PATCH 142/402] Evals groupKey and attach it to Entity model --- src/main/java/org/commcare/cases/entity/Entity.java | 12 +++++++++++- .../org/commcare/cases/entity/NodeEntityFactory.java | 9 ++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/Entity.java b/src/main/java/org/commcare/cases/entity/Entity.java index bb610f3117..d83960de33 100755 --- a/src/main/java/org/commcare/cases/entity/Entity.java +++ b/src/main/java/org/commcare/cases/entity/Entity.java @@ -2,6 +2,8 @@ import org.commcare.cases.util.StringUtils; +import javax.annotation.Nullable; + /** * @author ctsims */ @@ -17,6 +19,8 @@ public class Entity { public final String extraKey; private boolean shouldReceiveFocus; + private String groupKey; + protected Entity(T t, String extraKey) { this.t = t; @@ -24,13 +28,14 @@ protected Entity(T t, String extraKey) { } public Entity(Object[] data, String[] sortData, boolean[] relevancyData, T t, - String extraKey, boolean shouldReceiveFocus) { + String extraKey, boolean shouldReceiveFocus, String groupKey) { this.t = t; this.sortData = sortData; this.data = data; this.relevancyData = relevancyData; this.extraKey = extraKey; this.shouldReceiveFocus = shouldReceiveFocus; + this.groupKey = groupKey; } public Object getField(int i) { @@ -112,4 +117,9 @@ public String toString() { } return sb.toString() + "\n" + super.toString(); } + + @Nullable + public String getGroupKey() { + return groupKey; + } } diff --git a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java index dea0d4b450..20bb4a6ba1 100755 --- a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java @@ -4,6 +4,7 @@ import org.commcare.cases.query.queryset.CurrentModelQuerySet; import org.commcare.suite.model.Detail; import org.commcare.suite.model.DetailField; +import org.commcare.suite.model.DetailGroup; import org.commcare.suite.model.Text; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.TreeReference; @@ -74,8 +75,14 @@ public Entity getEntity(TreeReference data) { count++; } + String groupKey = null; + DetailGroup detailGroup = detail.getGroup(); + if (detailGroup != null) { + groupKey = (String)detailGroup.getFunction().eval(nodeContext); + } + return new Entity<>(fieldData, sortData, relevancyData, data, extraKey, - detail.evaluateFocusFunction(nodeContext)); + detail.evaluateFocusFunction(nodeContext), groupKey); } /** From 65fb4d48419abca993a4dc361516cd7619deaca4 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 6 Jun 2023 20:07:56 -0400 Subject: [PATCH 143/402] sort entities set by groupKey --- .../util/screen/EntityScreenHelper.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java b/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java index 878eee0d78..69c078be53 100644 --- a/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java @@ -10,7 +10,11 @@ import org.javarosa.core.model.instance.TreeReference; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Common methods for initialising entities @@ -36,6 +40,18 @@ public static List> initEntities(EvaluationContext context nodeEntityFactory.prepareEntities(entities); entities = filterEntities(entityScreenContext, nodeEntityFactory, entities); sortEntities(entityScreenContext, entities, detail); + return groupEntities(entities, detail); + } + + // Sorts by order of appearance of a groupKey if grouping is enabled + private static List> groupEntities(List> entities, Detail detail) { + if (detail.getGroup() != null) { + Map groupKeyOrder = entities.stream() + .collect(HashMap::new, + (m, entity) -> m.putIfAbsent(entity.getGroupKey(), m.size()), + (m1, m2) -> m2.keySet().forEach(l->m1.putIfAbsent(l, m1.size()))); + entities.sort(Comparator.comparingInt(entity -> groupKeyOrder.get(entity.getGroupKey()))); + } return entities; } @@ -78,7 +94,7 @@ private static void sortEntities(EntityScreenContext entityScreenContext, } } } - java.util.Collections.sort(entities, + Collections.sort(entities, new EntitySorter(shortDetail.getFields(), reverse, order, new LogNotifier())); } From 5efa7f709174b8322cfce35052f7b4ede8d414b6 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Wed, 19 Apr 2023 16:05:31 -0400 Subject: [PATCH 144/402] add handling assertions in menus --- .../java/org/commcare/suite/model/Menu.java | 12 +++++- .../org/commcare/suite/model/MenuLoader.java | 7 +++- .../java/org/commcare/xml/MenuParser.java | 38 +++++++++++------- .../backend/session/test/MenuTests.java | 39 +++++++++++++++++++ .../suite/model/test/AppStructureTests.java | 8 ++++ src/test/resources/app_structure/suite.xml | 12 ++++++ 6 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 src/test/java/org/commcare/backend/session/test/MenuTests.java diff --git a/src/main/java/org/commcare/suite/model/Menu.java b/src/main/java/org/commcare/suite/model/Menu.java index 72bdfcdbb5..2e2c838762 100644 --- a/src/main/java/org/commcare/suite/model/Menu.java +++ b/src/main/java/org/commcare/suite/model/Menu.java @@ -4,6 +4,7 @@ import org.javarosa.core.util.externalizable.DeserializationException; import org.javarosa.core.util.externalizable.ExtUtil; import org.javarosa.core.util.externalizable.ExtWrapList; +import org.javarosa.core.util.externalizable.ExtWrapNullable; import org.javarosa.core.util.externalizable.Externalizable; import org.javarosa.core.util.externalizable.PrototypeFactory; import org.javarosa.xpath.XPathParseTool; @@ -36,6 +37,7 @@ public class Menu implements Externalizable, MenuDisplayable { private String rawRelevance; private String style; private XPathExpression relevance; + AssertionSet assertions; /** * Serialization only!!! @@ -46,7 +48,8 @@ public Menu() { public Menu(String id, String root, String rawRelevance, XPathExpression relevance, DisplayUnit display, - Vector commandIds, String[] commandExprs, String style) { + Vector commandIds, String[] commandExprs, + String style, AssertionSet assertions) { this.id = id; this.root = root; this.rawRelevance = rawRelevance; @@ -55,6 +58,7 @@ public Menu(String id, String root, String rawRelevance, this.commandIds = commandIds; this.commandExprs = commandExprs; this.style = style; + this.assertions = assertions; } /** @@ -115,6 +119,10 @@ public XPathExpression getCommandRelevance(int index) throws XPathSyntaxExceptio return commandExprs[index] == null ? null : XPathParseTool.parseXPath(commandExprs[index]); } + public AssertionSet getAssertions() { + return assertions == null ? new AssertionSet(new Vector(), new Vector()) : assertions; + } + /** * @return an optional string indicating how this menu wants to display its items */ @@ -146,6 +154,7 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) } } style = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); + assertions = (AssertionSet)ExtUtil.read(in, new ExtWrapNullable(AssertionSet.class), pf); } @Override @@ -166,6 +175,7 @@ public void writeExternal(DataOutputStream out) throws IOException { } ExtUtil.writeString(out, ExtUtil.emptyIfNull(style)); + ExtUtil.write(out, new ExtWrapNullable(assertions)); } diff --git a/src/main/java/org/commcare/suite/model/MenuLoader.java b/src/main/java/org/commcare/suite/model/MenuLoader.java index c355e771a6..1e62222c69 100644 --- a/src/main/java/org/commcare/suite/model/MenuLoader.java +++ b/src/main/java/org/commcare/suite/model/MenuLoader.java @@ -73,7 +73,7 @@ private void getMenuDisplayables(CommCarePlatform platform, for (Menu m : s.getMenus()) { try { if (m.getId().equals(menuID)) { - if (menuIsRelevant(sessionWrapper, m)) { + if (menuIsRelevant(sessionWrapper, m) && menuAssertionsPass(sessionWrapper, m)) { addRelevantCommandEntries(sessionWrapper, m, items, badges, map, includeBadges); } } else { @@ -145,6 +145,11 @@ private boolean menuIsRelevant(SessionWrapperInterface sessionWrapper, Menu m) t return true; } + public boolean menuAssertionsPass(SessionWrapperInterface sessionWrapper, Menu m) { + Text text = m.getAssertions().getAssertionFailure(sessionWrapper.getEvaluationContext()); + return text == null; + } + private void addRelevantCommandEntries(SessionWrapperInterface sessionWrapper, Menu m, Vector items, diff --git a/src/main/java/org/commcare/xml/MenuParser.java b/src/main/java/org/commcare/xml/MenuParser.java index 20e2b90e67..d1f74115c7 100644 --- a/src/main/java/org/commcare/xml/MenuParser.java +++ b/src/main/java/org/commcare/xml/MenuParser.java @@ -1,5 +1,6 @@ package org.commcare.xml; +import org.commcare.suite.model.AssertionSet; import org.commcare.suite.model.DisplayUnit; import org.commcare.suite.model.Menu; import org.javarosa.xml.util.InvalidStructureException; @@ -39,12 +40,13 @@ public Menu parse() throws InvalidStructureException, IOException, XmlPullParser throw new InvalidStructureException("Bad module filtering expression {" + relevant + "}", parser); } } + AssertionSet assertions = null; String style = parser.getAttributeValue(null, "style"); getNextTagInBlock("menu"); - DisplayUnit display; + DisplayUnit display = null; if (parser.getName().equals("text")) { display = new DisplayUnit(new TextParser(parser).parse()); } else if (parser.getName().equals("display")) { @@ -62,19 +64,28 @@ public Menu parse() throws InvalidStructureException, IOException, XmlPullParser Vector commandIds = new Vector<>(); Vector relevantExprs = new Vector<>(); while (nextTagInBlock("menu")) { - checkNode("command"); - commandIds.addElement(parser.getAttributeValue(null, "id")); - String relevantExpr = parser.getAttributeValue(null, "relevant"); - if (relevantExpr == null) { - relevantExprs.addElement(null); - } else { + String tagName = parser.getName(); + if (tagName.equals("command")) { + commandIds.addElement(parser.getAttributeValue(null, "id")); + String relevantExpr = parser.getAttributeValue(null, "relevant"); + if (relevantExpr == null) { + relevantExprs.addElement(null); + } else { + try { + //Safety checking + XPathParseTool.parseXPath(relevantExpr); + relevantExprs.addElement(relevantExpr); + } catch (XPathSyntaxException e) { + e.printStackTrace(); + throw new InvalidStructureException("Bad XPath Expression {" + relevantExpr + "}", parser); + } + } + } else if (tagName.equals("assertions")) { try { - //Safety checking - XPathParseTool.parseXPath(relevantExpr); - relevantExprs.addElement(relevantExpr); - } catch (XPathSyntaxException e) { + assertions = new AssertionSetParser(parser).parse(); + } catch (InvalidStructureException e) { e.printStackTrace(); - throw new InvalidStructureException("Bad XPath Expression {" + relevantExpr + "}", parser); + throw new InvalidStructureException(e.getMessage(), parser); } } } @@ -82,6 +93,7 @@ public Menu parse() throws InvalidStructureException, IOException, XmlPullParser String[] expressions = new String[relevantExprs.size()]; relevantExprs.copyInto(expressions); - return new Menu(id, root, relevant, relevantExpression, display, commandIds, expressions, style); + return new Menu(id, root, relevant, relevantExpression, display, commandIds, expressions, + style, assertions); } } diff --git a/src/test/java/org/commcare/backend/session/test/MenuTests.java b/src/test/java/org/commcare/backend/session/test/MenuTests.java new file mode 100644 index 0000000000..5ecb9d94e8 --- /dev/null +++ b/src/test/java/org/commcare/backend/session/test/MenuTests.java @@ -0,0 +1,39 @@ +package org.commcare.backend.session.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.commcare.suite.model.AssertionSet; +import org.commcare.suite.model.Menu; +import org.commcare.suite.model.Suite; +import org.commcare.suite.model.Text; +import org.commcare.test.utilities.MockApp; +import org.javarosa.core.model.condition.EvaluationContext; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for assertions in menus + */ +public class MenuTests { + + MockApp appWithMenuAssertions; + Suite suite; + + @Before + public void setup() throws Exception { + appWithMenuAssertions = new MockApp("/app_structure/"); + suite = appWithMenuAssertions.getSession().getPlatform().getInstalledSuites().get(0); + } + + + @Test + public void testAssertionsEvaluated() throws Exception { + Menu menuWithAssertionsBlock = suite.getMenusWithId("m0").get(0); + AssertionSet assertions = menuWithAssertionsBlock.getAssertions(); + EvaluationContext ec = appWithMenuAssertions.getSession().getEvaluationContext(); + Text assertionFailures = assertions.getAssertionFailure(ec); + assertNotNull(assertions); + assertEquals("custom_assertion.m0.0", assertionFailures.getArgument()); + } +} diff --git a/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java b/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java index d710cac8d3..4a602a6808 100644 --- a/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java +++ b/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java @@ -1,6 +1,7 @@ package org.commcare.backend.suite.model.test; import org.commcare.resources.model.UnresolvedResourceException; +import org.commcare.suite.model.AssertionSet; import org.commcare.suite.model.Callout; import org.commcare.suite.model.DetailField; import org.commcare.suite.model.GeoOverlay; @@ -199,4 +200,11 @@ public void testDisplayBlockParsing_invalidXPathExpr() throws Exception { } } + @Test + public void testMenuAssertions() { + Suite s = mApp.getSession().getPlatform().getInstalledSuites().get(0); + Menu menuWithAssertionsBlock = s.getMenusWithId("m0").get(0); + AssertionSet assertions = menuWithAssertionsBlock.getAssertions(); + Assert.assertNotNull(assertions); + } } diff --git a/src/test/resources/app_structure/suite.xml b/src/test/resources/app_structure/suite.xml index 9aef2f86e5..e104dd4df9 100644 --- a/src/test/resources/app_structure/suite.xml +++ b/src/test/resources/app_structure/suite.xml @@ -186,6 +186,18 @@ Menu + + + + + + + + + + + + From 89eb8c9baf435c6c98e16202cd7be118d802ea1c Mon Sep 17 00:00:00 2001 From: robert-costello Date: Fri, 21 Apr 2023 16:38:09 -0400 Subject: [PATCH 145/402] update menu load exception on assertion failure --- src/main/java/org/commcare/suite/model/MenuLoader.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/suite/model/MenuLoader.java b/src/main/java/org/commcare/suite/model/MenuLoader.java index 1e62222c69..d927fa6d38 100644 --- a/src/main/java/org/commcare/suite/model/MenuLoader.java +++ b/src/main/java/org/commcare/suite/model/MenuLoader.java @@ -147,7 +147,10 @@ private boolean menuIsRelevant(SessionWrapperInterface sessionWrapper, Menu m) t public boolean menuAssertionsPass(SessionWrapperInterface sessionWrapper, Menu m) { Text text = m.getAssertions().getAssertionFailure(sessionWrapper.getEvaluationContext()); - return text == null; + if (text != null) { + loadException = new Exception(text.evaluate()); + } + return true; } private void addRelevantCommandEntries(SessionWrapperInterface sessionWrapper, From 9e144c6b270dadcbb90c337352e17c729fb16183 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Tue, 25 Apr 2023 11:10:36 -0400 Subject: [PATCH 146/402] return false on failed assertion --- src/main/java/org/commcare/suite/model/MenuLoader.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/commcare/suite/model/MenuLoader.java b/src/main/java/org/commcare/suite/model/MenuLoader.java index d927fa6d38..bc312f8763 100644 --- a/src/main/java/org/commcare/suite/model/MenuLoader.java +++ b/src/main/java/org/commcare/suite/model/MenuLoader.java @@ -149,6 +149,7 @@ public boolean menuAssertionsPass(SessionWrapperInterface sessionWrapper, Menu m Text text = m.getAssertions().getAssertionFailure(sessionWrapper.getEvaluationContext()); if (text != null) { loadException = new Exception(text.evaluate()); + return false; } return true; } From 7ea4e43e70a41cb54cb56bccdc2ba42a208a5b5e Mon Sep 17 00:00:00 2001 From: robert-costello Date: Thu, 27 Apr 2023 17:13:06 -0400 Subject: [PATCH 147/402] update eval context --- .../commcare/suite/model/AssertionSet.java | 4 +++ .../org/commcare/suite/model/MenuLoader.java | 28 +++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/AssertionSet.java b/src/main/java/org/commcare/suite/model/AssertionSet.java index ab337cb8e2..84526b2d58 100644 --- a/src/main/java/org/commcare/suite/model/AssertionSet.java +++ b/src/main/java/org/commcare/suite/model/AssertionSet.java @@ -65,6 +65,10 @@ public Text getAssertionFailure(EvaluationContext ec) { } } + public Vector getAssertionsXPaths() { + return this.xpathExpressions; + } + @Override public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { this.xpathExpressions = (Vector)ExtUtil.read(in, new ExtWrapList(String.class), pf); diff --git a/src/main/java/org/commcare/suite/model/MenuLoader.java b/src/main/java/org/commcare/suite/model/MenuLoader.java index bc312f8763..70a6761f2d 100644 --- a/src/main/java/org/commcare/suite/model/MenuLoader.java +++ b/src/main/java/org/commcare/suite/model/MenuLoader.java @@ -8,6 +8,7 @@ import org.javarosa.core.model.trace.ReducingTraceReporter; import org.javarosa.core.model.utils.InstrumentationUtils; import org.javarosa.xpath.XPathException; +import org.javarosa.xpath.XPathParseTool; import org.javarosa.xpath.XPathTypeMismatchException; import org.javarosa.xpath.analysis.InstanceNameAccumulatingAnalyzer; import org.javarosa.xpath.expr.FunctionUtils; @@ -145,11 +146,28 @@ private boolean menuIsRelevant(SessionWrapperInterface sessionWrapper, Menu m) t return true; } - public boolean menuAssertionsPass(SessionWrapperInterface sessionWrapper, Menu m) { - Text text = m.getAssertions().getAssertionFailure(sessionWrapper.getEvaluationContext()); - if (text != null) { - loadException = new Exception(text.evaluate()); - return false; + public boolean menuAssertionsPass(SessionWrapperInterface sessionWrapper, Menu m) throws XPathSyntaxException{ + Vector assertionXpathStrings = m.getAssertions().getAssertionsXPaths(); + if (!assertionXpathStrings.isEmpty()) { + XPathExpression assertionXpath = XPathParseTool.parseXPath(assertionXpathStrings.get(0)); + + Set instancesNeededByAssertionCondition = + (new InstanceNameAccumulatingAnalyzer()).accumulate(assertionXpath); + EvaluationContext ec = sessionWrapper.getRestrictedEvaluationContext(m.getId(), + instancesNeededByAssertionCondition); + + EvaluationContext traceableContext = new EvaluationContext(ec, ec.getOriginalContext()); + if (traceReporter != null) { + traceableContext.setDebugModeOn(traceReporter); + } + + InstrumentationUtils.printAndClearTraces(traceReporter, "menu load expand"); + Text text = m.getAssertions().getAssertionFailure(ec); + + if (text != null) { + loadException = new Exception(text.evaluate()); + return false; + } } return true; } From 344b82700e14f3f713d3c1c874e0f981985255e5 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Mon, 1 May 2023 15:45:43 -0400 Subject: [PATCH 148/402] gather all instances from seertions --- .../org/commcare/suite/model/MenuLoader.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/MenuLoader.java b/src/main/java/org/commcare/suite/model/MenuLoader.java index 70a6761f2d..126954830d 100644 --- a/src/main/java/org/commcare/suite/model/MenuLoader.java +++ b/src/main/java/org/commcare/suite/model/MenuLoader.java @@ -15,6 +15,7 @@ import org.javarosa.xpath.expr.XPathExpression; import org.javarosa.xpath.parser.XPathSyntaxException; +import java.util.HashSet; import java.util.Hashtable; import java.util.Set; import java.util.Vector; @@ -147,14 +148,18 @@ private boolean menuIsRelevant(SessionWrapperInterface sessionWrapper, Menu m) t } public boolean menuAssertionsPass(SessionWrapperInterface sessionWrapper, Menu m) throws XPathSyntaxException{ - Vector assertionXpathStrings = m.getAssertions().getAssertionsXPaths(); - if (!assertionXpathStrings.isEmpty()) { - XPathExpression assertionXpath = XPathParseTool.parseXPath(assertionXpathStrings.get(0)); + Vector assertionXPathStrings = m.getAssertions().getAssertionsXPaths(); + if (!assertionXPathStrings.isEmpty()) { + Set allInstancesNeededByAssertionCondition = new HashSet<>(); + for ( int i = 0; i < assertionXPathStrings.size(); i++) { + XPathExpression assertionXPath = XPathParseTool.parseXPath(assertionXPathStrings.get(i)); + Set instancesNeededByAssertionCondition = + (new InstanceNameAccumulatingAnalyzer()).accumulate(assertionXPath); + allInstancesNeededByAssertionCondition.addAll(instancesNeededByAssertionCondition); + } - Set instancesNeededByAssertionCondition = - (new InstanceNameAccumulatingAnalyzer()).accumulate(assertionXpath); EvaluationContext ec = sessionWrapper.getRestrictedEvaluationContext(m.getId(), - instancesNeededByAssertionCondition); + allInstancesNeededByAssertionCondition); EvaluationContext traceableContext = new EvaluationContext(ec, ec.getOriginalContext()); if (traceReporter != null) { From f5b6a26aeb3759cc5c2d6c590f95eb65ba84519a Mon Sep 17 00:00:00 2001 From: robert-costello Date: Mon, 1 May 2023 17:00:31 -0400 Subject: [PATCH 149/402] add commcare session instance --- src/main/java/org/commcare/suite/model/MenuLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/suite/model/MenuLoader.java b/src/main/java/org/commcare/suite/model/MenuLoader.java index 126954830d..dab897ae34 100644 --- a/src/main/java/org/commcare/suite/model/MenuLoader.java +++ b/src/main/java/org/commcare/suite/model/MenuLoader.java @@ -157,7 +157,7 @@ public boolean menuAssertionsPass(SessionWrapperInterface sessionWrapper, Menu m (new InstanceNameAccumulatingAnalyzer()).accumulate(assertionXPath); allInstancesNeededByAssertionCondition.addAll(instancesNeededByAssertionCondition); } - + allInstancesNeededByAssertionCondition.add("commcaresession"); EvaluationContext ec = sessionWrapper.getRestrictedEvaluationContext(m.getId(), allInstancesNeededByAssertionCondition); From a6f395c22371314d0ce3851f3176a59613973dce Mon Sep 17 00:00:00 2001 From: robert-costello Date: Tue, 23 May 2023 14:29:07 -0400 Subject: [PATCH 150/402] reverts previous two commits --- .../java/org/commcare/suite/model/MenuLoader.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/MenuLoader.java b/src/main/java/org/commcare/suite/model/MenuLoader.java index dab897ae34..0eee231fa1 100644 --- a/src/main/java/org/commcare/suite/model/MenuLoader.java +++ b/src/main/java/org/commcare/suite/model/MenuLoader.java @@ -150,16 +150,12 @@ private boolean menuIsRelevant(SessionWrapperInterface sessionWrapper, Menu m) t public boolean menuAssertionsPass(SessionWrapperInterface sessionWrapper, Menu m) throws XPathSyntaxException{ Vector assertionXPathStrings = m.getAssertions().getAssertionsXPaths(); if (!assertionXPathStrings.isEmpty()) { - Set allInstancesNeededByAssertionCondition = new HashSet<>(); - for ( int i = 0; i < assertionXPathStrings.size(); i++) { - XPathExpression assertionXPath = XPathParseTool.parseXPath(assertionXPathStrings.get(i)); - Set instancesNeededByAssertionCondition = - (new InstanceNameAccumulatingAnalyzer()).accumulate(assertionXPath); - allInstancesNeededByAssertionCondition.addAll(instancesNeededByAssertionCondition); - } - allInstancesNeededByAssertionCondition.add("commcaresession"); + XPathExpression assertionXPath = XPathParseTool.parseXPath(assertionXPathStrings.get(0)); + + Set instancesNeededByAssertionCondition = + (new InstanceNameAccumulatingAnalyzer()).accumulate(assertionXPath); EvaluationContext ec = sessionWrapper.getRestrictedEvaluationContext(m.getId(), - allInstancesNeededByAssertionCondition); + instancesNeededByAssertionCondition); EvaluationContext traceableContext = new EvaluationContext(ec, ec.getOriginalContext()); if (traceReporter != null) { From 57b96f25e99bc411f26ac3055cf25dddbea51ae6 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Tue, 13 Jun 2023 14:07:18 -0400 Subject: [PATCH 151/402] add parseInstace class --- src/main/java/org/commcare/xml/EntryParser.java | 8 +------- src/main/java/org/commcare/xml/MenuParser.java | 10 ++++++++-- src/main/java/org/commcare/xml/ParseInstance.java | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/commcare/xml/ParseInstance.java diff --git a/src/main/java/org/commcare/xml/EntryParser.java b/src/main/java/org/commcare/xml/EntryParser.java index 583f9ec3b9..d57cca8352 100644 --- a/src/main/java/org/commcare/xml/EntryParser.java +++ b/src/main/java/org/commcare/xml/EntryParser.java @@ -83,7 +83,7 @@ public Entry parse() throws InvalidStructureException, IOException, XmlPullParse commandId = parser.getAttributeValue(null, "id"); display = parseCommandDisplay(); } else if ("instance".equals(tagName.toLowerCase())) { - parseInstance(instances); + ParseInstance.parseInstance(instances, parser); } else if ("session".equals(tagName)) { parseSessionData(data); } else if ("entity".equals(tagName) || "details".equals(tagName)) { @@ -154,12 +154,6 @@ private void parseStack(Vector stackOps) throws InvalidStructure } } - private void parseInstance(Hashtable instances) { - String instanceId = parser.getAttributeValue(null, "id"); - String location = parser.getAttributeValue(null, "src"); - instances.put(instanceId, new ExternalDataInstance(location, instanceId)); - } - private PostRequest parsePost() throws InvalidStructureException, IOException, XmlPullParserException { String urlString = parser.getAttributeValue(null, "url"); if (urlString == null) { diff --git a/src/main/java/org/commcare/xml/MenuParser.java b/src/main/java/org/commcare/xml/MenuParser.java index d1f74115c7..9482431b92 100644 --- a/src/main/java/org/commcare/xml/MenuParser.java +++ b/src/main/java/org/commcare/xml/MenuParser.java @@ -3,6 +3,7 @@ import org.commcare.suite.model.AssertionSet; import org.commcare.suite.model.DisplayUnit; import org.commcare.suite.model.Menu; +import org.javarosa.core.model.instance.DataInstance; import org.javarosa.xml.util.InvalidStructureException; import org.javarosa.xpath.XPathParseTool; import org.javarosa.xpath.expr.XPathExpression; @@ -11,6 +12,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.util.Hashtable; import java.util.Vector; /** @@ -30,6 +32,8 @@ public Menu parse() throws InvalidStructureException, IOException, XmlPullParser String root = parser.getAttributeValue(null, "root"); root = root == null ? "root" : root; + Hashtable instances = new Hashtable<>(); + String relevant = parser.getAttributeValue(null, "relevant"); XPathExpression relevantExpression = null; if (relevant != null) { @@ -80,7 +84,9 @@ public Menu parse() throws InvalidStructureException, IOException, XmlPullParser throw new InvalidStructureException("Bad XPath Expression {" + relevantExpr + "}", parser); } } - } else if (tagName.equals("assertions")) { + } else if (tagName.toLowerCase().equals("instance")) { + ParseInstance.parseInstance(instances, parser); + }else if (tagName.equals("assertions")) { try { assertions = new AssertionSetParser(parser).parse(); } catch (InvalidStructureException e) { @@ -94,6 +100,6 @@ public Menu parse() throws InvalidStructureException, IOException, XmlPullParser relevantExprs.copyInto(expressions); return new Menu(id, root, relevant, relevantExpression, display, commandIds, expressions, - style, assertions); + style, assertions, instances); } } diff --git a/src/main/java/org/commcare/xml/ParseInstance.java b/src/main/java/org/commcare/xml/ParseInstance.java new file mode 100644 index 0000000000..203bc628a2 --- /dev/null +++ b/src/main/java/org/commcare/xml/ParseInstance.java @@ -0,0 +1,15 @@ +package org.commcare.xml; + +import org.javarosa.core.model.instance.DataInstance; +import org.javarosa.core.model.instance.ExternalDataInstance; +import org.kxml2.io.KXmlParser; + +import java.util.Hashtable; + +public class ParseInstance { + static void parseInstance(Hashtable instances, KXmlParser parser) { + String instanceId = parser.getAttributeValue(null, "id"); + String location = parser.getAttributeValue(null, "src"); + instances.put(instanceId, new ExternalDataInstance(location, instanceId)); + } +} From 07319f96df8717fbbbb325737799120399f166f3 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Tue, 13 Jun 2023 14:12:48 -0400 Subject: [PATCH 152/402] add instance accumulator method --- .../org/commcare/suite/model/MenuLoader.java | 97 ++++++++++--------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/MenuLoader.java b/src/main/java/org/commcare/suite/model/MenuLoader.java index 0eee231fa1..2b9ccc0714 100644 --- a/src/main/java/org/commcare/suite/model/MenuLoader.java +++ b/src/main/java/org/commcare/suite/model/MenuLoader.java @@ -15,8 +15,9 @@ import org.javarosa.xpath.expr.XPathExpression; import org.javarosa.xpath.parser.XPathSyntaxException; -import java.util.HashSet; +import java.util.HashMap; import java.util.Hashtable; +import java.util.Map; import java.util.Set; import java.util.Vector; @@ -34,15 +35,14 @@ public class MenuLoader { private ReducingTraceReporter traceReporter; - public MenuLoader(CommCarePlatform platform, SessionWrapperInterface sessionWrapper, - String menuId, LoggerInterface loggerInterface, - boolean shouldOutputEvalTrace, boolean hideTrainingRoot) { + public MenuLoader(CommCarePlatform platform, SessionWrapperInterface sessionWrapper, String menuId, + LoggerInterface loggerInterface, boolean shouldOutputEvalTrace, boolean hideTrainingRoot) { this(platform, sessionWrapper, menuId, loggerInterface, shouldOutputEvalTrace, hideTrainingRoot, false); } - public MenuLoader(CommCarePlatform platform, SessionWrapperInterface sessionWrapper, - String menuId, LoggerInterface loggerInterface, - boolean shouldOutputEvalTrace, boolean hideTrainingRoot, boolean includeBadges) { + public MenuLoader(CommCarePlatform platform, SessionWrapperInterface sessionWrapper, String menuId, + LoggerInterface loggerInterface, boolean shouldOutputEvalTrace, boolean hideTrainingRoot, + boolean includeBadges) { this.loggerInterface = loggerInterface; if (shouldOutputEvalTrace) { this.traceReporter = new ReducingTraceReporter(false); @@ -60,14 +60,15 @@ public String getErrorMessage() { } private void getMenuDisplayables(CommCarePlatform platform, - SessionWrapperInterface sessionWrapper, - String menuID, boolean hideTrainingRoot) { - getMenuDisplayables(platform,sessionWrapper, menuID, hideTrainingRoot, false); + SessionWrapperInterface sessionWrapper, + String menuID, boolean hideTrainingRoot) { + getMenuDisplayables(platform, sessionWrapper, menuID, hideTrainingRoot, false); } + private void getMenuDisplayables(CommCarePlatform platform, - SessionWrapperInterface sessionWrapper, - String menuID, boolean hideTrainingRoot, - boolean includeBadges) { + SessionWrapperInterface sessionWrapper, + String menuID, boolean hideTrainingRoot, + boolean includeBadges) { Vector items = new Vector<>(); Vector badges = new Vector<>(); Hashtable map = platform.getCommandToEntryMap(); @@ -82,7 +83,7 @@ private void getMenuDisplayables(CommCarePlatform platform, addUnaddedMenu(sessionWrapper, menuID, m, items, badges, hideTrainingRoot, includeBadges); } } catch (CommCareInstanceInitializer.FixtureInitializationException - | XPathSyntaxException | XPathException xpe) { + | XPathSyntaxException | XPathException xpe) { setLoadException(xpe); menus = new MenuDisplayable[0]; return; @@ -98,8 +99,8 @@ private void getMenuDisplayables(CommCarePlatform platform, } private void addUnaddedMenu(SessionWrapperInterface sessionWrapper, String currentMenuId, - Menu toAdd, Vector items, Vector badges, - boolean hideTrainingRoot, boolean includeBadges) throws XPathSyntaxException { + Menu toAdd, Vector items, Vector badges, + boolean hideTrainingRoot, boolean includeBadges) throws XPathSyntaxException { if (hideTrainingRoot && toAdd.getId().equals(Menu.TRAINING_MENU_ROOT)) { return; } @@ -130,15 +131,7 @@ private boolean menuIsRelevant(SessionWrapperInterface sessionWrapper, Menu m) t if (m.getMenuRelevance() != null) { xPathErrorMessage = m.getMenuRelevanceRaw(); - Set instancesNeededByRelevancyCondition = - (new InstanceNameAccumulatingAnalyzer()).accumulate(relevance); - EvaluationContext ec = sessionWrapper.getRestrictedEvaluationContext(m.getId(), - instancesNeededByRelevancyCondition); - - EvaluationContext traceableContext = new EvaluationContext(ec, ec.getOriginalContext()); - if (traceReporter != null) { - traceableContext.setDebugModeOn(traceReporter); - } + EvaluationContext traceableContext = accumulateInstances(sessionWrapper, m, relevance); boolean result = FunctionUtils.toBoolean(relevance.eval(traceableContext)); InstrumentationUtils.printAndClearTraces(traceReporter, "menu load expand"); @@ -147,38 +140,48 @@ private boolean menuIsRelevant(SessionWrapperInterface sessionWrapper, Menu m) t return true; } - public boolean menuAssertionsPass(SessionWrapperInterface sessionWrapper, Menu m) throws XPathSyntaxException{ - Vector assertionXPathStrings = m.getAssertions().getAssertionsXPaths(); + public boolean menuAssertionsPass(SessionWrapperInterface sessionWrapper, Menu m) throws XPathSyntaxException { + AssertionSet assertions = m.getAssertions(); + Vector assertionXPathStrings = assertions.getAssertionsXPaths(); if (!assertionXPathStrings.isEmpty()) { - XPathExpression assertionXPath = XPathParseTool.parseXPath(assertionXPathStrings.get(0)); - Set instancesNeededByAssertionCondition = - (new InstanceNameAccumulatingAnalyzer()).accumulate(assertionXPath); - EvaluationContext ec = sessionWrapper.getRestrictedEvaluationContext(m.getId(), - instancesNeededByAssertionCondition); - - EvaluationContext traceableContext = new EvaluationContext(ec, ec.getOriginalContext()); - if (traceReporter != null) { - traceableContext.setDebugModeOn(traceReporter); - } + for (int i = 0; i < assertionXPathStrings.size(); i++) { + XPathExpression assertionXPath = XPathParseTool.parseXPath(assertionXPathStrings.get(i)); + EvaluationContext traceableContext = accumulateInstances(sessionWrapper, m, assertionXPath); - InstrumentationUtils.printAndClearTraces(traceReporter, "menu load expand"); - Text text = m.getAssertions().getAssertionFailure(ec); + Text text = assertions.evalAssertionAtIndex(i, assertionXPath, traceableContext); - if (text != null) { - loadException = new Exception(text.evaluate()); - return false; + InstrumentationUtils.printAndClearTraces(traceReporter, "menu load expand"); + if (text != null) { + loadException = new Exception(text.evaluate()); + return false; + } } } return true; } + private EvaluationContext accumulateInstances( + SessionWrapperInterface sessionWrapper, + Menu m, + XPathExpression xPathExpression) { + Set instancesNeededByCondition = + (new InstanceNameAccumulatingAnalyzer()).accumulate(xPathExpression); + EvaluationContext ec = sessionWrapper.getRestrictedEvaluationContext(m.getId(), + m.getInstances(instancesNeededByCondition).keySet()); + EvaluationContext traceableContext = new EvaluationContext(ec, ec.getOriginalContext()); + if (traceReporter != null) { + traceableContext.setDebugModeOn(traceReporter); + } + return traceableContext; + } + private void addRelevantCommandEntries(SessionWrapperInterface sessionWrapper, - Menu m, - Vector items, - Vector badges, - Hashtable map, - boolean includeBadges) + Menu m, + Vector items, + Vector badges, + Hashtable map, + boolean includeBadges) throws XPathSyntaxException { xPathErrorMessage = ""; for (String command : m.getCommandIds()) { From e28127c64ac33fa6912806b518324bbb6619fc18 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Tue, 13 Jun 2023 14:15:19 -0400 Subject: [PATCH 153/402] move getLimitedInstances to utils --- .../java/org/commcare/suite/model/Entry.java | 43 +++---------------- .../java/org/commcare/suite/model/Menu.java | 21 +++++++-- .../model/instance/utils/InstanceUtils.java | 39 +++++++++++++++-- 3 files changed, 60 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/Entry.java b/src/main/java/org/commcare/suite/model/Entry.java index 53ee06f64f..1d8b6c985a 100644 --- a/src/main/java/org/commcare/suite/model/Entry.java +++ b/src/main/java/org/commcare/suite/model/Entry.java @@ -2,7 +2,7 @@ import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.DataInstance; -import org.javarosa.core.model.instance.ExternalDataInstance; +import org.javarosa.core.model.instance.utils.InstanceUtils; import org.javarosa.core.util.externalizable.DeserializationException; import org.javarosa.core.util.externalizable.ExtUtil; import org.javarosa.core.util.externalizable.ExtWrapList; @@ -16,7 +16,6 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; -import java.util.Enumeration; import java.util.Hashtable; import java.util.Set; import java.util.Vector; @@ -45,10 +44,10 @@ public Entry() { } public Entry(String commandId, DisplayUnit display, - Vector data, - Hashtable instances, - Vector stackOperations, - AssertionSet assertions) { + Vector data, + Hashtable instances, + Vector stackOperations, + AssertionSet assertions) { this.commandId = commandId == null ? "" : commandId; this.display = display; this.data = data; @@ -93,36 +92,8 @@ public Vector getSessionDataReqs() { return data; } - public Hashtable getInstances() { - return getInstances(null); - } - /** - * - * @param limitingList a list of instance names to restrict the returning set to; null - * if no limiting is being used - * @return a hashtable representing the data instances that are in scope for this Entry, - * potentially limited by @limitingList - */ - public Hashtable getInstances(Set limitingList) { - Hashtable copy = new Hashtable<>(); - for (Enumeration en = instances.keys(); en.hasMoreElements(); ) { - String key = (String)en.nextElement(); - - //This is silly, all of these are external data instances. TODO: save their - //construction details instead. - DataInstance cur = instances.get(key); - if (limitingList == null || limitingList.contains(cur.getInstanceId())) { - // Make sure we either aren't using a limiting list, or the instanceid is in the list - if (cur instanceof ExternalDataInstance) { - //Copy the EDI so when it gets populated we don't keep it dependent on this object's lifecycle!! - copy.put(key, new ExternalDataInstance(((ExternalDataInstance)cur).getReference(), cur.getInstanceId())); - } else { - copy.put(key, cur); - } - } - } - - return copy; + public Hashtable getInstances(Set instancesToInclude) { + return InstanceUtils.getLimitedInstances(null, instances); } public AssertionSet getAssertions() { diff --git a/src/main/java/org/commcare/suite/model/Menu.java b/src/main/java/org/commcare/suite/model/Menu.java index 2e2c838762..12531e5dff 100644 --- a/src/main/java/org/commcare/suite/model/Menu.java +++ b/src/main/java/org/commcare/suite/model/Menu.java @@ -1,10 +1,14 @@ package org.commcare.suite.model; import org.javarosa.core.model.condition.EvaluationContext; +import org.javarosa.core.model.instance.DataInstance; +import org.javarosa.core.model.instance.utils.InstanceUtils; import org.javarosa.core.util.externalizable.DeserializationException; import org.javarosa.core.util.externalizable.ExtUtil; import org.javarosa.core.util.externalizable.ExtWrapList; +import org.javarosa.core.util.externalizable.ExtWrapMap; import org.javarosa.core.util.externalizable.ExtWrapNullable; +import org.javarosa.core.util.externalizable.ExtWrapTagged; import org.javarosa.core.util.externalizable.Externalizable; import org.javarosa.core.util.externalizable.PrototypeFactory; import org.javarosa.xpath.XPathParseTool; @@ -14,6 +18,8 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.util.Hashtable; +import java.util.Set; import java.util.Vector; import io.reactivex.Single; @@ -38,6 +44,7 @@ public class Menu implements Externalizable, MenuDisplayable { private String style; private XPathExpression relevance; AssertionSet assertions; + Hashtable instances; /** * Serialization only!!! @@ -47,9 +54,10 @@ public Menu() { } public Menu(String id, String root, String rawRelevance, - XPathExpression relevance, DisplayUnit display, - Vector commandIds, String[] commandExprs, - String style, AssertionSet assertions) { + XPathExpression relevance, DisplayUnit display, + Vector commandIds, String[] commandExprs, + String style, AssertionSet assertions, + Hashtable instances) { this.id = id; this.root = root; this.rawRelevance = rawRelevance; @@ -59,6 +67,7 @@ public Menu(String id, String root, String rawRelevance, this.commandExprs = commandExprs; this.style = style; this.assertions = assertions; + this.instances = instances; } /** @@ -119,6 +128,10 @@ public XPathExpression getCommandRelevance(int index) throws XPathSyntaxExceptio return commandExprs[index] == null ? null : XPathParseTool.parseXPath(commandExprs[index]); } + public Hashtable getInstances(Set instancesToInclude) { + return InstanceUtils.getLimitedInstances(null, instances); + } + public AssertionSet getAssertions() { return assertions == null ? new AssertionSet(new Vector(), new Vector()) : assertions; } @@ -147,6 +160,7 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) rawRelevance = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); display = (DisplayUnit)ExtUtil.read(in, DisplayUnit.class, pf); commandIds = (Vector)ExtUtil.read(in, new ExtWrapList(String.class), pf); + instances = (Hashtable)ExtUtil.read(in, new ExtWrapMap(String.class, new ExtWrapTagged()), pf); commandExprs = new String[ExtUtil.readInt(in)]; for (int i = 0; i < commandExprs.length; ++i) { if (ExtUtil.readBool(in)) { @@ -164,6 +178,7 @@ public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.writeString(out, ExtUtil.emptyIfNull(rawRelevance)); ExtUtil.write(out, display); ExtUtil.write(out, new ExtWrapList(commandIds)); + ExtUtil.write(out, new ExtWrapMap(instances, new ExtWrapTagged())); ExtUtil.writeNumeric(out, commandExprs.length); for (String commandExpr : commandExprs) { if (commandExpr == null) { diff --git a/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java index 763de62565..8778cbe0b7 100644 --- a/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java +++ b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java @@ -2,8 +2,9 @@ import static org.javarosa.core.model.instance.utils.TreeUtilities.xmlToTreeElement; -import org.commcare.cases.instance.CaseInstanceTreeElement; import org.javarosa.core.model.instance.AbstractTreeElement; +import org.javarosa.core.model.instance.DataInstance; +import org.javarosa.core.model.instance.ExternalDataInstance; import org.javarosa.core.model.instance.FormInstance; import org.javarosa.core.model.instance.InstanceBase; import org.javarosa.core.model.instance.TreeElement; @@ -11,6 +12,9 @@ import org.javarosa.xml.util.InvalidStructureException; import java.io.IOException; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Set; /** * Collection of static instance loading methods @@ -38,13 +42,40 @@ public static void setUpInstanceRoot(AbstractTreeElement instanceRoot, String in TreeElement rootAsTreeElement = ((TreeElement)instanceRoot); rootAsTreeElement.setInstanceName(instanceId); rootAsTreeElement.setParent(instanceBase); - } else if (instanceRoot instanceof CaseInstanceTreeElement) { - CaseInstanceTreeElement caseInstanceRoot = ((CaseInstanceTreeElement)instanceRoot); - caseInstanceRoot.rebase(instanceBase); } else { String error = "Unrecognised Instance root of type " + instanceRoot.getClass().getName() + " for instance " + instanceId; throw new UnrecognisedInstanceRootException(error); } } + + /** + * + * @param limitingList a list of instance names to restrict the returning set to; null + * if no limiting is being used + * @return a hashtable representing the data instances that are in scope for this Entry, + * potentially limited by @limitingList + */ + public static Hashtable getLimitedInstances(Set limitingList, + Hashtable instances) { + Hashtable copy = new Hashtable<>(); + for (Enumeration en = instances.keys(); en.hasMoreElements(); ) { + String key = (String)en.nextElement(); + + //This is silly, all of these are external data instances. TODO: save their + //construction details instead. + DataInstance cur = instances.get(key); + if (limitingList == null || limitingList.contains(cur.getInstanceId())) { + // Make sure we either aren't using a limiting list, or the instanceid is in the list + if (cur instanceof ExternalDataInstance) { + //Copy the EDI so when it gets populated we don't keep it dependent on this object's lifecycle!! + copy.put(key, new ExternalDataInstance(((ExternalDataInstance)cur).getReference(), cur.getInstanceId())); + } else { + copy.put(key, cur); + } + } + } + + return copy; + } } From 7b8c017bf3a769e71f377dd23d34d103547c5873 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Tue, 13 Jun 2023 14:18:24 -0400 Subject: [PATCH 154/402] add method evalAssertionAtIndex --- .../org/commcare/suite/model/AssertionSet.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/org/commcare/suite/model/AssertionSet.java b/src/main/java/org/commcare/suite/model/AssertionSet.java index 84526b2d58..630c66e546 100644 --- a/src/main/java/org/commcare/suite/model/AssertionSet.java +++ b/src/main/java/org/commcare/suite/model/AssertionSet.java @@ -69,6 +69,20 @@ public Vector getAssertionsXPaths() { return this.xpathExpressions; } + public Text evalAssertionAtIndex(Integer i, XPathExpression expression, EvaluationContext ec) { + try { + Object val = expression.eval(ec); + if (!FunctionUtils.toBoolean(val)) { + return messages.elementAt(i); + } + } catch (Exception e) { + // Following the patter from getAssertionFailure. Is there a reason not to throw the exception here? + return messages.elementAt(i); + } + + return null; + } + @Override public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { this.xpathExpressions = (Vector)ExtUtil.read(in, new ExtWrapList(String.class), pf); From c119b69eb7c788207d9a04d0f04770a193a126fe Mon Sep 17 00:00:00 2001 From: robert-costello Date: Tue, 13 Jun 2023 14:22:20 -0400 Subject: [PATCH 155/402] add method getMenusForCommand, update getEvaluationContext --- .../org/commcare/session/CommCareSession.java | 59 ++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/commcare/session/CommCareSession.java b/src/main/java/org/commcare/session/CommCareSession.java index 6ee17338bc..76b5cb7bcd 100644 --- a/src/main/java/org/commcare/session/CommCareSession.java +++ b/src/main/java/org/commcare/session/CommCareSession.java @@ -151,6 +151,18 @@ private Vector getEntriesForCommand(String commandId) { return entries; } + + private Vector getMenusForCommand(String commandId) { + Vector menusWithId = new Vector<>(); + for (Suite s : platform.getInstalledSuites()) { + List menus = s.getMenusWithId(commandId); + if (menus != null && menus.size() > 0) { + menusWithId.addAll(s.getMenusWithId(commandId)); + } + } + return menusWithId; + } + @Nullable public FormEntry getEntryForNameSpace(String xmlns) { for (Suite suite : platform.getInstalledSuites()) { @@ -341,7 +353,7 @@ public SessionDatum getNeededDatum(Entry entry) { * datumsCollectedSoFar */ private SessionDatum getFirstMissingDatum(OrderedHashtable datumsCollectedSoFar, - Vector allDatumsNeeded) { + Vector allDatumsNeeded) { for (SessionDatum datum : allDatumsNeeded) { if (!datumsCollectedSoFar.containsKey(datum.getDataId())) { return datum; @@ -613,20 +625,39 @@ public EvaluationContext getEvaluationContext(InstanceInitializationFactory iif) * @return Evaluation context for a command in the installed app */ public EvaluationContext getEvaluationContext(InstanceInitializationFactory iif, - String command, - Set instancesToInclude) { + String command, + Set instancesToInclude) { if (command == null) { return new EvaluationContext(null); } Vector entries = getEntriesForCommand(command); - - if (entries.size() == 0) { - return new EvaluationContext(null); + Vector menus = getMenusForCommand(command); + + Menu menu = null; + Entry entry = null; + Hashtable instancesInScope = new Hashtable<>(); + Hashtable menuInstances = null; + Hashtable entryInstances = null; + + if (!entries.isEmpty()) { + entry = entries.elementAt(0); + if (entry != null) { + entryInstances = entry.getInstances(instancesToInclude); + } + if (entryInstances != null) { + instancesInScope.putAll(entryInstances); + } } - Entry entry = entries.elementAt(0); - - Hashtable instancesInScope = entry.getInstances(instancesToInclude); + if (!menus.isEmpty()) { + menu = menus.elementAt(0); + if (menu != null) { + menuInstances = menu.getInstances(instancesToInclude); + } + if (menuInstances != null) { + instancesInScope.putAll(menuInstances); + } + } for (Enumeration en = instancesInScope.keys(); en.hasMoreElements(); ) { String key = (String)en.nextElement(); @@ -638,7 +669,7 @@ public EvaluationContext getEvaluationContext(InstanceInitializationFactory iif, } private void addInstancesFromFrame(Hashtable instanceMap, - InstanceInitializationFactory iif) { + InstanceInitializationFactory iif) { for (StackFrameStep step : frame.getSteps()) { instanceMap.putAll(step.getInstances(iif)); } @@ -716,7 +747,7 @@ private boolean processStackOp(StackOperation op, EvaluationContext ec, StackObs } private void createFrame(SessionFrame createdFrame, - StackOperation op, EvaluationContext ec, StackObserver observer) { + StackOperation op, EvaluationContext ec, StackObserver observer) { if (op.isOperationTriggered(ec)) { // create has its own event so don't pass through the active observer performPushInner(op, createdFrame, ec, new StackObserver()); @@ -760,7 +791,7 @@ private void pushFrameStep(StackFrameStep step, SessionFrame frame, EvaluationCo } private static SessionDatum getNeededDatumForFrame(CommCareSession session, - SessionFrame targetFrame) { + SessionFrame targetFrame) { CommCareSession sessionCopy = new CommCareSession(session); sessionCopy.frame = targetFrame; sessionCopy.syncState(); @@ -793,7 +824,7 @@ private void pushNewFrame(SessionFrame matchingFrame, StackObserver observer) { } private void performClearOperation(StackOperation op, - EvaluationContext ec, StackObserver observer) { + EvaluationContext ec, StackObserver observer) { if (op.isOperationTriggered(ec)) { frameStack.removeElement(frame); observer.dropped(frame); @@ -1028,7 +1059,7 @@ public Multimap getCurrentFrameStepExtras() { * Doesn't support restoring the frame stack */ public static CommCareSession restoreSessionFromStream(CommCarePlatform ccPlatform, - DataInputStream inputStream) + DataInputStream inputStream) throws DeserializationException, IOException { SessionFrame restoredFrame = new SessionFrame(); restoredFrame.readExternal(inputStream, ExtUtil.defaultPrototypes()); From cc365e10ec34bd6094626c00e3f72a54203ddc29 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Wed, 14 Jun 2023 18:56:59 -0400 Subject: [PATCH 156/402] remove logical loop --- src/main/java/org/commcare/suite/model/MenuLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/suite/model/MenuLoader.java b/src/main/java/org/commcare/suite/model/MenuLoader.java index 2b9ccc0714..1d68260216 100644 --- a/src/main/java/org/commcare/suite/model/MenuLoader.java +++ b/src/main/java/org/commcare/suite/model/MenuLoader.java @@ -168,7 +168,7 @@ private EvaluationContext accumulateInstances( Set instancesNeededByCondition = (new InstanceNameAccumulatingAnalyzer()).accumulate(xPathExpression); EvaluationContext ec = sessionWrapper.getRestrictedEvaluationContext(m.getId(), - m.getInstances(instancesNeededByCondition).keySet()); + instancesNeededByCondition); EvaluationContext traceableContext = new EvaluationContext(ec, ec.getOriginalContext()); if (traceReporter != null) { traceableContext.setDebugModeOn(traceReporter); From 16987988ebb9d23407e6b80fb5cb9bdfe28b0ce7 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Fri, 16 Jun 2023 11:30:57 -0400 Subject: [PATCH 157/402] remove unused fixture and update tests --- .../org/commcare/backend/session/test/EvalContextTests.java | 2 ++ src/test/resources/app_with_menu_display_conditions/suite.xml | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/commcare/backend/session/test/EvalContextTests.java b/src/test/java/org/commcare/backend/session/test/EvalContextTests.java index 9dc01c0df5..d71fae8bef 100644 --- a/src/test/java/org/commcare/backend/session/test/EvalContextTests.java +++ b/src/test/java/org/commcare/backend/session/test/EvalContextTests.java @@ -46,6 +46,7 @@ public void testRestrictedEvalContextGeneration1() throws Exception { // 1) Confirm that the eval context was restricted properly List instancesThatShouldBeIncluded = new ArrayList<>(); instancesThatShouldBeIncluded.add("casedb"); + instancesThatShouldBeIncluded.add("commcaresession"); Assert.assertEquals(instancesThatShouldBeIncluded, ec.getInstanceIds()); // 2) Confirm the display condition was evaluated properly with the restricted context @@ -67,6 +68,7 @@ public void testRestrictedEvalContextGeneration2() throws Exception { // 1) Confirm that the eval context was restricted properly List instancesThatShouldBeIncluded = new ArrayList<>(); + instancesThatShouldBeIncluded.add("casedb"); instancesThatShouldBeIncluded.add("commcaresession"); Assert.assertEquals(instancesThatShouldBeIncluded, ec.getInstanceIds()); diff --git a/src/test/resources/app_with_menu_display_conditions/suite.xml b/src/test/resources/app_with_menu_display_conditions/suite.xml index 2d349c6263..ea91078af2 100644 --- a/src/test/resources/app_with_menu_display_conditions/suite.xml +++ b/src/test/resources/app_with_menu_display_conditions/suite.xml @@ -107,7 +107,6 @@ - @@ -121,7 +120,6 @@ - From 3daf3043f66b1edb0a7183ab63132f3071968cee Mon Sep 17 00:00:00 2001 From: robert-costello Date: Tue, 20 Jun 2023 11:24:55 -0400 Subject: [PATCH 158/402] undo possible bad merge --- .../org/javarosa/core/model/instance/utils/InstanceUtils.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java index 8778cbe0b7..a3c1e11dea 100644 --- a/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java +++ b/src/main/java/org/javarosa/core/model/instance/utils/InstanceUtils.java @@ -2,6 +2,7 @@ import static org.javarosa.core.model.instance.utils.TreeUtilities.xmlToTreeElement; +import org.commcare.cases.instance.CaseInstanceTreeElement; import org.javarosa.core.model.instance.AbstractTreeElement; import org.javarosa.core.model.instance.DataInstance; import org.javarosa.core.model.instance.ExternalDataInstance; @@ -42,6 +43,9 @@ public static void setUpInstanceRoot(AbstractTreeElement instanceRoot, String in TreeElement rootAsTreeElement = ((TreeElement)instanceRoot); rootAsTreeElement.setInstanceName(instanceId); rootAsTreeElement.setParent(instanceBase); + } else if (instanceRoot instanceof CaseInstanceTreeElement) { + CaseInstanceTreeElement caseInstanceRoot = ((CaseInstanceTreeElement)instanceRoot); + caseInstanceRoot.rebase(instanceBase); } else { String error = "Unrecognised Instance root of type " + instanceRoot.getClass().getName() + " for instance " + instanceId; From 3fa74d9c5287fd4a9ba86434fc15a4eaa45af559 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Mon, 3 Jul 2023 13:53:08 -0400 Subject: [PATCH 159/402] remove comment --- src/main/java/org/commcare/suite/model/AssertionSet.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/commcare/suite/model/AssertionSet.java b/src/main/java/org/commcare/suite/model/AssertionSet.java index 630c66e546..0d05e0412d 100644 --- a/src/main/java/org/commcare/suite/model/AssertionSet.java +++ b/src/main/java/org/commcare/suite/model/AssertionSet.java @@ -76,7 +76,6 @@ public Text evalAssertionAtIndex(Integer i, XPathExpression expression, Evaluati return messages.elementAt(i); } } catch (Exception e) { - // Following the patter from getAssertionFailure. Is there a reason not to throw the exception here? return messages.elementAt(i); } From c79960e43085031072cabab5f3a78906a9ca3bcf Mon Sep 17 00:00:00 2001 From: robert-costello Date: Mon, 3 Jul 2023 15:16:52 -0400 Subject: [PATCH 160/402] fix instance limiting calls and related tests --- src/main/java/org/commcare/suite/model/Entry.java | 2 +- src/main/java/org/commcare/suite/model/Menu.java | 2 +- .../org/commcare/backend/session/test/EvalContextTests.java | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/Entry.java b/src/main/java/org/commcare/suite/model/Entry.java index 1d8b6c985a..aa02c31549 100644 --- a/src/main/java/org/commcare/suite/model/Entry.java +++ b/src/main/java/org/commcare/suite/model/Entry.java @@ -93,7 +93,7 @@ public Vector getSessionDataReqs() { } public Hashtable getInstances(Set instancesToInclude) { - return InstanceUtils.getLimitedInstances(null, instances); + return InstanceUtils.getLimitedInstances(instancesToInclude, instances); } public AssertionSet getAssertions() { diff --git a/src/main/java/org/commcare/suite/model/Menu.java b/src/main/java/org/commcare/suite/model/Menu.java index 12531e5dff..17474d6809 100644 --- a/src/main/java/org/commcare/suite/model/Menu.java +++ b/src/main/java/org/commcare/suite/model/Menu.java @@ -129,7 +129,7 @@ public XPathExpression getCommandRelevance(int index) throws XPathSyntaxExceptio } public Hashtable getInstances(Set instancesToInclude) { - return InstanceUtils.getLimitedInstances(null, instances); + return InstanceUtils.getLimitedInstances(instancesToInclude, instances); } public AssertionSet getAssertions() { diff --git a/src/test/java/org/commcare/backend/session/test/EvalContextTests.java b/src/test/java/org/commcare/backend/session/test/EvalContextTests.java index d71fae8bef..9dc01c0df5 100644 --- a/src/test/java/org/commcare/backend/session/test/EvalContextTests.java +++ b/src/test/java/org/commcare/backend/session/test/EvalContextTests.java @@ -46,7 +46,6 @@ public void testRestrictedEvalContextGeneration1() throws Exception { // 1) Confirm that the eval context was restricted properly List instancesThatShouldBeIncluded = new ArrayList<>(); instancesThatShouldBeIncluded.add("casedb"); - instancesThatShouldBeIncluded.add("commcaresession"); Assert.assertEquals(instancesThatShouldBeIncluded, ec.getInstanceIds()); // 2) Confirm the display condition was evaluated properly with the restricted context @@ -68,7 +67,6 @@ public void testRestrictedEvalContextGeneration2() throws Exception { // 1) Confirm that the eval context was restricted properly List instancesThatShouldBeIncluded = new ArrayList<>(); - instancesThatShouldBeIncluded.add("casedb"); instancesThatShouldBeIncluded.add("commcaresession"); Assert.assertEquals(instancesThatShouldBeIncluded, ec.getInstanceIds()); From ca0f8d99da0a3f172cb36794ddfaa28293bfc17b Mon Sep 17 00:00:00 2001 From: robert-costello Date: Mon, 3 Jul 2023 15:20:35 -0400 Subject: [PATCH 161/402] update trace description to be more clear --- src/main/java/org/commcare/suite/model/MenuLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/suite/model/MenuLoader.java b/src/main/java/org/commcare/suite/model/MenuLoader.java index 1d68260216..e76a3a3b18 100644 --- a/src/main/java/org/commcare/suite/model/MenuLoader.java +++ b/src/main/java/org/commcare/suite/model/MenuLoader.java @@ -151,7 +151,7 @@ public boolean menuAssertionsPass(SessionWrapperInterface sessionWrapper, Menu m Text text = assertions.evalAssertionAtIndex(i, assertionXPath, traceableContext); - InstrumentationUtils.printAndClearTraces(traceReporter, "menu load expand"); + InstrumentationUtils.printAndClearTraces(traceReporter, "menu assertions"); if (text != null) { loadException = new Exception(text.evaluate()); return false; From b04055e6f38442369f6a37d3e50dfefb0e9eb72f Mon Sep 17 00:00:00 2001 From: robert-costello Date: Mon, 3 Jul 2023 15:24:38 -0400 Subject: [PATCH 162/402] remove unneeded empty check --- .../org/commcare/suite/model/MenuLoader.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/MenuLoader.java b/src/main/java/org/commcare/suite/model/MenuLoader.java index e76a3a3b18..3260084b1c 100644 --- a/src/main/java/org/commcare/suite/model/MenuLoader.java +++ b/src/main/java/org/commcare/suite/model/MenuLoader.java @@ -143,19 +143,17 @@ private boolean menuIsRelevant(SessionWrapperInterface sessionWrapper, Menu m) t public boolean menuAssertionsPass(SessionWrapperInterface sessionWrapper, Menu m) throws XPathSyntaxException { AssertionSet assertions = m.getAssertions(); Vector assertionXPathStrings = assertions.getAssertionsXPaths(); - if (!assertionXPathStrings.isEmpty()) { - for (int i = 0; i < assertionXPathStrings.size(); i++) { - XPathExpression assertionXPath = XPathParseTool.parseXPath(assertionXPathStrings.get(i)); - EvaluationContext traceableContext = accumulateInstances(sessionWrapper, m, assertionXPath); + for (int i = 0; i < assertionXPathStrings.size(); i++) { + XPathExpression assertionXPath = XPathParseTool.parseXPath(assertionXPathStrings.get(i)); + EvaluationContext traceableContext = accumulateInstances(sessionWrapper, m, assertionXPath); - Text text = assertions.evalAssertionAtIndex(i, assertionXPath, traceableContext); + Text text = assertions.evalAssertionAtIndex(i, assertionXPath, traceableContext); - InstrumentationUtils.printAndClearTraces(traceReporter, "menu assertions"); - if (text != null) { - loadException = new Exception(text.evaluate()); - return false; - } + InstrumentationUtils.printAndClearTraces(traceReporter, "menu assertions"); + if (text != null) { + loadException = new Exception(text.evaluate()); + return false; } } return true; From 668e8b4c3eb763f16ddd09a904841d76aeb7febf Mon Sep 17 00:00:00 2001 From: robert-costello Date: Mon, 3 Jul 2023 15:41:08 -0400 Subject: [PATCH 163/402] add fixture back --- src/test/resources/app_with_menu_display_conditions/suite.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/resources/app_with_menu_display_conditions/suite.xml b/src/test/resources/app_with_menu_display_conditions/suite.xml index ea91078af2..2d349c6263 100644 --- a/src/test/resources/app_with_menu_display_conditions/suite.xml +++ b/src/test/resources/app_with_menu_display_conditions/suite.xml @@ -107,6 +107,7 @@ + @@ -120,6 +121,7 @@ + From 0695e45d3f5fd929a2faca0bacc320c6a43572af Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Fri, 5 May 2023 23:30:25 +0200 Subject: [PATCH 164/402] Add FCM log type --- src/main/java/org/commcare/util/LogTypes.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/commcare/util/LogTypes.java b/src/main/java/org/commcare/util/LogTypes.java index 3afbe378f7..14e84040c5 100644 --- a/src/main/java/org/commcare/util/LogTypes.java +++ b/src/main/java/org/commcare/util/LogTypes.java @@ -107,4 +107,9 @@ public class LogTypes { public static final String TYPE_CC_UPDATE = "commcare-update"; + /** + * Logs related to Firebase Cloud Messaging + */ + public static final String TYPE_FCM = "fcm"; + } From 960f2cf899f5cb1bde035a7f87225700f1a5905a Mon Sep 17 00:00:00 2001 From: robert-costello Date: Mon, 10 Jul 2023 17:13:02 -0400 Subject: [PATCH 165/402] add resultsTitle to model --- .../org/commcare/suite/model/RemoteQueryDatum.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/RemoteQueryDatum.java b/src/main/java/org/commcare/suite/model/RemoteQueryDatum.java index c31b16d7f3..9d25636373 100644 --- a/src/main/java/org/commcare/suite/model/RemoteQueryDatum.java +++ b/src/main/java/org/commcare/suite/model/RemoteQueryDatum.java @@ -32,6 +32,7 @@ public class RemoteQueryDatum extends SessionDatum { private boolean defaultSearch; private Text title; private Text description; + private Text resultsTitle; @SuppressWarnings("unused") public RemoteQueryDatum() { @@ -45,7 +46,8 @@ public RemoteQueryDatum() { public RemoteQueryDatum(URL url, String storageInstance, List hiddenQueryValues, OrderedHashtable userQueryPrompts, - boolean useCaseTemplate, boolean defaultSearch, Text title, Text description) { + boolean useCaseTemplate, boolean defaultSearch, + Text title, Text description, Text resultsTitle) { super(storageInstance, url.toString()); this.hiddenQueryValues = hiddenQueryValues; this.userQueryPrompts = userQueryPrompts; @@ -53,6 +55,7 @@ public RemoteQueryDatum(URL url, String storageInstance, this.defaultSearch = defaultSearch; this.title = title; this.description = description; + this.resultsTitle = resultsTitle; } public OrderedHashtable getUserQueryPrompts() { @@ -89,6 +92,8 @@ public Text getDescriptionText() { return description; } + public Text getResultsTitleText() { return resultsTitle; } + @Override public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { @@ -102,6 +107,7 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) description = (Text) ExtUtil.read(in, new ExtWrapNullable(Text.class), pf); useCaseTemplate = ExtUtil.readBool(in); defaultSearch = ExtUtil.readBool(in); + resultsTitle = (Text) ExtUtil.read(in, new ExtWrapNullable(Text.class), pf); } @@ -114,6 +120,6 @@ public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.write(out, new ExtWrapNullable(description)); ExtUtil.writeBool(out, useCaseTemplate); ExtUtil.writeBool(out, defaultSearch); - + ExtUtil.write(out, new ExtWrapNullable(resultsTitle)); } } From 8e4e948e14090c5a1a2f6dff76ee53c98f6b01d0 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Mon, 10 Jul 2023 17:13:59 -0400 Subject: [PATCH 166/402] parse resultsTitle --- src/main/java/org/commcare/xml/SessionDatumParser.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/xml/SessionDatumParser.java b/src/main/java/org/commcare/xml/SessionDatumParser.java index 9dcb161fb8..378e44f197 100644 --- a/src/main/java/org/commcare/xml/SessionDatumParser.java +++ b/src/main/java/org/commcare/xml/SessionDatumParser.java @@ -128,6 +128,7 @@ private RemoteQueryDatum parseRemoteQueryDatum() boolean defaultSearch = "true".equals(parser.getAttributeValue(null, "default_search")); Text title = null; Text description = null; + Text resultsTitle = null; ArrayList hiddenQueryValues = new ArrayList(); while (nextTagInBlock("query")) { String tagName = parser.getName(); @@ -142,9 +143,16 @@ private RemoteQueryDatum parseRemoteQueryDatum() } else if ("description".equals(tagName)) { nextTagInBlock("description"); description = new TextParser(parser).parse(); + } else if ("results_title".equals(tagName)) { + nextTagInBlock("results_title"); + resultsTitle = new TextParser(parser).parse(); + } else if ("results-title".equals(tagName)) { + nextTagInBlock("results-title"); + resultsTitle = new TextParser(parser).parse(); } } return new RemoteQueryDatum(queryUrl, queryResultStorageInstance, - hiddenQueryValues, userQueryPrompts, useCaseTemplate, defaultSearch, title, description); + hiddenQueryValues, userQueryPrompts, useCaseTemplate, defaultSearch, title, description, + resultsTitle); } } From 5bd2215ef7ce75f5ece77b1a97a9ee0de11ade4c Mon Sep 17 00:00:00 2001 From: robert-costello Date: Mon, 10 Jul 2023 17:14:45 -0400 Subject: [PATCH 167/402] add results title to query screen --- .../org/commcare/util/screen/QueryScreen.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/cli/java/org/commcare/util/screen/QueryScreen.java b/src/cli/java/org/commcare/util/screen/QueryScreen.java index 7eee1ee2e2..542561c4d0 100644 --- a/src/cli/java/org/commcare/util/screen/QueryScreen.java +++ b/src/cli/java/org/commcare/util/screen/QueryScreen.java @@ -51,6 +51,7 @@ public class QueryScreen extends Screen { private String[] fields; private String mTitle; private String description; + private String resultsTitle; private String currentMessage; private String domainedUsername; @@ -94,6 +95,7 @@ public void init(SessionWrapper sessionWrapper) throws CommCareSessionException mTitle = getTitleLocaleString(); description = getDescriptionLocaleString(); + resultsTitle = getResultsTitleLocaleString(); } private String getTitleLocaleString() { @@ -114,6 +116,15 @@ private String getDescriptionLocaleString() { return description; } + private String getResultsTitleLocaleString() { + try { + resultsTitle = getQueryDatum().getResultsTitleText().evaluate(); + } catch (NoLocalizedTextException | NullPointerException e) { + resultsTitle = ""; + } + return resultsTitle; + } + private String getTitleLocaleStringLegacy() { try { mTitle = Localization.get("case.search.title"); @@ -237,6 +248,10 @@ public String getDescriptionText() { return description; } + public String getResultsTitle() { + return resultsTitle; + } + @Override public boolean prompt(PrintStream out) { if (doDefaultSearch()) { From bcd907cf39e5a6285524021defef850550b9ac0f Mon Sep 17 00:00:00 2001 From: robert-costello Date: Mon, 10 Jul 2023 17:15:15 -0400 Subject: [PATCH 168/402] update test --- .../backend/suite/model/test/CaseClaimModelTests.java | 3 +++ src/test/resources/case_claim_example/app_strings.txt | 3 ++- src/test/resources/case_claim_example/suite.xml | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java b/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java index 53f597b368..0bb9f408f7 100644 --- a/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java +++ b/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java @@ -62,6 +62,9 @@ public void testRemoteQueryDatum() throws Exception { Text description = ((RemoteQueryDatum) datum).getDescriptionText(); Assert.assertEquals("Description text", description.evaluate()); + + Text resultsTitle = ((RemoteQueryDatum) datum).getResultsTitleText(); + Assert.assertEquals("Results Title", resultsTitle.evaluate()); } @Test diff --git a/src/test/resources/case_claim_example/app_strings.txt b/src/test/resources/case_claim_example/app_strings.txt index 0c06c4a2dd..5768caf89f 100644 --- a/src/test/resources/case_claim_example/app_strings.txt +++ b/src/test/resources/case_claim_example/app_strings.txt @@ -6,4 +6,5 @@ query.another_age.validation.message=another age should be greater than 18 query.age.required.message=One of age or DOB is required query.dob.required.message=One of age or DOB is required query.title=Title Label -query.description=Description text \ No newline at end of file +query.description=Description text +query.resultsTitle=Results Title \ No newline at end of file diff --git a/src/test/resources/case_claim_example/suite.xml b/src/test/resources/case_claim_example/suite.xml index ee0ba9c4ac..2336f8c6ca 100644 --- a/src/test/resources/case_claim_example/suite.xml +++ b/src/test/resources/case_claim_example/suite.xml @@ -72,6 +72,11 @@ + + + + + From 3dc69aedd8b00d65c316449baa2e91e0659f26aa Mon Sep 17 00:00:00 2001 From: robert-costello Date: Mon, 10 Jul 2023 17:24:03 -0400 Subject: [PATCH 169/402] remove extra else block --- src/main/java/org/commcare/xml/SessionDatumParser.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/org/commcare/xml/SessionDatumParser.java b/src/main/java/org/commcare/xml/SessionDatumParser.java index 378e44f197..e26b436ebd 100644 --- a/src/main/java/org/commcare/xml/SessionDatumParser.java +++ b/src/main/java/org/commcare/xml/SessionDatumParser.java @@ -143,9 +143,6 @@ private RemoteQueryDatum parseRemoteQueryDatum() } else if ("description".equals(tagName)) { nextTagInBlock("description"); description = new TextParser(parser).parse(); - } else if ("results_title".equals(tagName)) { - nextTagInBlock("results_title"); - resultsTitle = new TextParser(parser).parse(); } else if ("results-title".equals(tagName)) { nextTagInBlock("results-title"); resultsTitle = new TextParser(parser).parse(); From 1ad1164e5f3b0704fbd3a220b53447972195abee Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 6 Jul 2023 17:21:35 -0400 Subject: [PATCH 170/402] Skip adding null values in session instance instead of crashing out --- .../java/org/commcare/session/SessionInstanceBuilder.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/commcare/session/SessionInstanceBuilder.java b/src/main/java/org/commcare/session/SessionInstanceBuilder.java index b3311fd814..1c7b5b54bf 100644 --- a/src/main/java/org/commcare/session/SessionInstanceBuilder.java +++ b/src/main/java/org/commcare/session/SessionInstanceBuilder.java @@ -116,8 +116,10 @@ private static void addUserProperties(TreeElement sessionRoot, private static void addData(TreeElement root, String name, String data) { TreeElement datum = new TreeElement(name, root.getChildMultiplicity(name)); - datum.setValue(new UncastData(data)); - root.addChild(datum); + if (data != null) { + datum.setValue(new UncastData(data)); + root.addChild(datum); + } } } From 4e6ab4ada073f3f788812e8f7b5a2ab60e3acc33 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 18 Jul 2023 11:01:26 +0200 Subject: [PATCH 171/402] Add network log type --- src/main/java/org/commcare/util/LogTypes.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/commcare/util/LogTypes.java b/src/main/java/org/commcare/util/LogTypes.java index 3afbe378f7..21c3316270 100644 --- a/src/main/java/org/commcare/util/LogTypes.java +++ b/src/main/java/org/commcare/util/LogTypes.java @@ -107,4 +107,6 @@ public class LogTypes { public static final String TYPE_CC_UPDATE = "commcare-update"; + public static final String TYPE_NETWORK = "commcare-network"; + } From 067827410f8486780f315eb025d51957a23dfc14 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 24 Jul 2023 19:59:14 +0200 Subject: [PATCH 172/402] Bump OkHttp to 4.11.0 --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 5b1b9d3f4c..f81ad5c875 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,7 @@ dependencies { implementation 'joda-time:joda-time:2.12.1' implementation 'com.carrotsearch:hppc:0.9.1' api 'com.squareup.retrofit2:retrofit:2.3.0' - api 'com.squareup.okhttp3:okhttp:3.12.12' + api 'com.squareup.okhttp3:okhttp:4.11.0' implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'io.reactivex.rxjava2:rxjava:2.2.21' implementation 'com.google.guava:guava:31.1-jre' @@ -90,7 +90,7 @@ dependencies { cliImplementation 'joda-time:joda-time:2.9.4' cliImplementation 'io.reactivex.rxjava2:rxjava:2.1.1' cliImplementation 'com.squareup.retrofit2:retrofit:2.3.0' - cliImplementation 'com.squareup.okhttp3:okhttp:4.4.1' + cliImplementation 'com.squareup.okhttp3:okhttp:4.11.0' cliImplementation 'com.google.guava:guava:31.1-jre' cliImplementation 'com.datadoghq:dd-trace-api:1.10.0' @@ -103,7 +103,7 @@ dependencies { testImplementation 'org.xerial:sqlite-jdbc:3.40.0.0' testImplementation 'joda-time:joda-time:2.9.4' testImplementation 'com.squareup.retrofit2:retrofit:2.3.0' - testImplementation 'com.squareup.okhttp3:okhttp:4.4.1' + testImplementation 'com.squareup.okhttp3:okhttp:4.11.0' testImplementation 'com.google.guava:guava:31.1-jre' ccapiImplementation sourceSets.main.output @@ -114,7 +114,7 @@ dependencies { ccapiImplementation 'com.carrotsearch:hppc:0.9.1' ccapiImplementation 'io.reactivex.rxjava2:rxjava:2.1.1' ccapiImplementation "com.squareup.retrofit2:retrofit:2.3.0" - ccapiImplementation 'com.squareup.okhttp3:okhttp:4.4.1' + ccapiImplementation 'com.squareup.okhttp3:okhttp:4.11.0' ccapiImplementation 'com.google.guava:guava:31.1-jre' ccapiImplementation 'io.opentracing:opentracing-api:0.33.0' ccapiImplementation 'io.opentracing:opentracing-util:0.33.0' @@ -128,7 +128,7 @@ dependencies { translateImplementation 'org.json:json:20140107' translateImplementation 'io.reactivex.rxjava2:rxjava:2.1.1' translateImplementation "com.squareup.retrofit2:retrofit:2.3.0" - translateImplementation 'com.squareup.okhttp3:okhttp:4.4.1' + translateImplementation 'com.squareup.okhttp3:okhttp:4.11.0' translateImplementation 'com.google.guava:guava:31.1-jre' // 'compile' dependency is also needed to run 'gradle jmh' for some reason, From 5302d5c80199594e3e731488af09ea62b0467d4b Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 25 Jul 2023 00:01:46 +0200 Subject: [PATCH 173/402] Refactor --- .../org/javarosa/core/model/utils/DateUtils.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/org/javarosa/core/model/utils/DateUtils.java b/src/main/java/org/javarosa/core/model/utils/DateUtils.java index 30b13b78fc..5d6337e0b6 100755 --- a/src/main/java/org/javarosa/core/model/utils/DateUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/DateUtils.java @@ -4,6 +4,8 @@ import org.javarosa.core.util.DataUtil; import org.javarosa.core.util.MathUtils; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Date; @@ -945,4 +947,14 @@ public static boolean stringContains(String string, String substring) { return string.contains(substring); } + // TODO: Move this method to DateUtils + public static String convertTimeInMsToISO8601(long ms) { + if (ms == 0) { + return ""; + } else { + DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + return df.format(ms); + } + } } From b8ce32ed094122638ab6a85d70094d4dbb3b94f6 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 10 Jul 2023 10:19:49 +0200 Subject: [PATCH 174/402] Block unsafe events when a background sync is underway --- .../org/commcare/session/SessionNavigationResponder.java | 3 +++ src/main/java/org/commcare/session/SessionNavigator.java | 7 +++++++ .../test/utilities/MockSessionNavigationResponder.java | 9 +++++++++ 3 files changed, 19 insertions(+) diff --git a/src/main/java/org/commcare/session/SessionNavigationResponder.java b/src/main/java/org/commcare/session/SessionNavigationResponder.java index 3ab504892a..4630479e87 100644 --- a/src/main/java/org/commcare/session/SessionNavigationResponder.java +++ b/src/main/java/org/commcare/session/SessionNavigationResponder.java @@ -2,6 +2,8 @@ import org.javarosa.core.model.condition.EvaluationContext; +import java.util.concurrent.locks.ReentrantLock; + /** * Interface defining all functionality to be implemented by any class that will receive and * process status codes from a SessionNavigator @@ -19,4 +21,5 @@ public interface SessionNavigationResponder { // Provide a hook to the current evaluation context that the SessionNavigator will use EvaluationContext getEvalContextForNavigator(); + ReentrantLock getBackgroundSyncLock(); } diff --git a/src/main/java/org/commcare/session/SessionNavigator.java b/src/main/java/org/commcare/session/SessionNavigator.java index b09ccea26b..cade818f3a 100644 --- a/src/main/java/org/commcare/session/SessionNavigator.java +++ b/src/main/java/org/commcare/session/SessionNavigator.java @@ -28,6 +28,7 @@ public class SessionNavigator { public static final int START_SYNC_REQUEST = 7; public static final int PROCESS_QUERY_REQUEST = 8; public static final int REPORT_CASE_AUTOSELECT = 9; + public static final int FORM_ENTRY_ATTEMPT_DURING_SYNC = 10; private final SessionNavigationResponder responder; private CommCareSession currentSession; @@ -107,6 +108,12 @@ private void readyToProceed() { else if (currentSession.getForm() == null) { sendResponse(NO_CURRENT_FORM); } else { + // The current state indicate that a form needs to be instantiated but a background sync is ongoing + if (responder.getBackgroundSyncLock().isLocked()) { + sendResponse(FORM_ENTRY_ATTEMPT_DURING_SYNC); + return; + } + sendResponse(START_FORM_ENTRY); } } diff --git a/src/test/java/org/commcare/test/utilities/MockSessionNavigationResponder.java b/src/test/java/org/commcare/test/utilities/MockSessionNavigationResponder.java index 64913365a5..14667f1418 100644 --- a/src/test/java/org/commcare/test/utilities/MockSessionNavigationResponder.java +++ b/src/test/java/org/commcare/test/utilities/MockSessionNavigationResponder.java @@ -5,6 +5,8 @@ import org.commcare.session.SessionNavigationResponder; import org.javarosa.core.model.condition.EvaluationContext; +import java.util.concurrent.locks.ReentrantLock; + /** * A mock implementer of the SessionNavigationResponder interface, for testing purposes * @@ -14,9 +16,11 @@ public class MockSessionNavigationResponder implements SessionNavigationResponde private final SessionWrapper sessionWrapper; private int lastReceivedResultCode; + private ReentrantLock backgroungSyncLock; public MockSessionNavigationResponder(SessionWrapper sessionWrapper) { this.sessionWrapper = sessionWrapper; + this.backgroungSyncLock = new ReentrantLock(); } public int getLastResultCode() { @@ -38,4 +42,9 @@ public EvaluationContext getEvalContextForNavigator() { return sessionWrapper.getEvaluationContext(); } + @Override + public ReentrantLock getBackgroundSyncLock() { + return backgroungSyncLock; + } + } From f84f11ffcbd1380f0a3478b78754205abe58e81c Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 13 Jul 2023 14:59:49 +0200 Subject: [PATCH 175/402] Skip certain restore elements during parsing --- .../org/commcare/data/xml/DataModelPullParser.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/commcare/data/xml/DataModelPullParser.java b/src/main/java/org/commcare/data/xml/DataModelPullParser.java index d76f26e0d2..ee9f3307c1 100644 --- a/src/main/java/org/commcare/data/xml/DataModelPullParser.java +++ b/src/main/java/org/commcare/data/xml/DataModelPullParser.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.LinkedHashSet; +import java.util.List; import java.util.Vector; /** @@ -28,13 +29,14 @@ public class DataModelPullParser extends ElementParser { private final InputStream is; private final String requiredRootEnvelope = null; private final CommCareOTARestoreListener rListener; + private List blocksToSkipParsing; public DataModelPullParser(InputStream is, TransactionParserFactory factory) throws InvalidStructureException, IOException { this(is, factory, false); } public DataModelPullParser(InputStream is, TransactionParserFactory factory, CommCareOTARestoreListener rl) throws InvalidStructureException, IOException { - this(is, factory, false, false, rl); + this(is, factory, false, false, rl, null); } public DataModelPullParser(InputStream is, TransactionParserFactory factory, boolean deep) throws InvalidStructureException, IOException { @@ -42,10 +44,10 @@ public DataModelPullParser(InputStream is, TransactionParserFactory factory, boo } public DataModelPullParser(InputStream is, TransactionParserFactory factory, boolean failfast, boolean deep) throws InvalidStructureException, IOException { - this(is, factory, failfast, deep, null); + this(is, factory, failfast, deep, null, null); } - public DataModelPullParser(InputStream is, TransactionParserFactory factory, boolean failfast, boolean deep, CommCareOTARestoreListener rListener) throws InvalidStructureException, IOException { + public DataModelPullParser(InputStream is, TransactionParserFactory factory, boolean failfast, boolean deep, CommCareOTARestoreListener rListener, List blocksToSkipParsing) throws InvalidStructureException, IOException { super(ElementParser.instantiateParser(is)); this.is = is; this.failfast = failfast; @@ -53,6 +55,7 @@ public DataModelPullParser(InputStream is, TransactionParserFactory factory, boo errors = new Vector<>(); this.deep = deep; this.rListener = rListener; + this.blocksToSkipParsing = blocksToSkipParsing; } @Override @@ -126,6 +129,11 @@ private void parseBlock(String root, LinkedHashSet parsers) t continue; } + if (blocksToSkipParsing != null && blocksToSkipParsing.equals(name)) { + this.skipBlock(name); + continue; + } + TransactionParser transaction = factory.getParser(parser); if (transaction == null) { if (deep) { From 34ed46090d981a2b45274f0df8e5e32e394db652 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Fri, 14 Jul 2023 15:10:37 +0200 Subject: [PATCH 176/402] Fix check around blocks to skip parsing --- src/main/java/org/commcare/data/xml/DataModelPullParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/data/xml/DataModelPullParser.java b/src/main/java/org/commcare/data/xml/DataModelPullParser.java index ee9f3307c1..7b2a1979d4 100644 --- a/src/main/java/org/commcare/data/xml/DataModelPullParser.java +++ b/src/main/java/org/commcare/data/xml/DataModelPullParser.java @@ -129,7 +129,7 @@ private void parseBlock(String root, LinkedHashSet parsers) t continue; } - if (blocksToSkipParsing != null && blocksToSkipParsing.equals(name)) { + if (blocksToSkipParsing != null && blocksToSkipParsing.contains(name)) { this.skipBlock(name); continue; } From 91a601df1c283c6f288ab925038d155ddcae6190 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 26 Jul 2023 17:51:21 +0200 Subject: [PATCH 177/402] Remove skip parsing logic --- .../org/commcare/data/xml/DataModelPullParser.java | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/commcare/data/xml/DataModelPullParser.java b/src/main/java/org/commcare/data/xml/DataModelPullParser.java index 7b2a1979d4..d76f26e0d2 100644 --- a/src/main/java/org/commcare/data/xml/DataModelPullParser.java +++ b/src/main/java/org/commcare/data/xml/DataModelPullParser.java @@ -10,7 +10,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.LinkedHashSet; -import java.util.List; import java.util.Vector; /** @@ -29,14 +28,13 @@ public class DataModelPullParser extends ElementParser { private final InputStream is; private final String requiredRootEnvelope = null; private final CommCareOTARestoreListener rListener; - private List blocksToSkipParsing; public DataModelPullParser(InputStream is, TransactionParserFactory factory) throws InvalidStructureException, IOException { this(is, factory, false); } public DataModelPullParser(InputStream is, TransactionParserFactory factory, CommCareOTARestoreListener rl) throws InvalidStructureException, IOException { - this(is, factory, false, false, rl, null); + this(is, factory, false, false, rl); } public DataModelPullParser(InputStream is, TransactionParserFactory factory, boolean deep) throws InvalidStructureException, IOException { @@ -44,10 +42,10 @@ public DataModelPullParser(InputStream is, TransactionParserFactory factory, boo } public DataModelPullParser(InputStream is, TransactionParserFactory factory, boolean failfast, boolean deep) throws InvalidStructureException, IOException { - this(is, factory, failfast, deep, null, null); + this(is, factory, failfast, deep, null); } - public DataModelPullParser(InputStream is, TransactionParserFactory factory, boolean failfast, boolean deep, CommCareOTARestoreListener rListener, List blocksToSkipParsing) throws InvalidStructureException, IOException { + public DataModelPullParser(InputStream is, TransactionParserFactory factory, boolean failfast, boolean deep, CommCareOTARestoreListener rListener) throws InvalidStructureException, IOException { super(ElementParser.instantiateParser(is)); this.is = is; this.failfast = failfast; @@ -55,7 +53,6 @@ public DataModelPullParser(InputStream is, TransactionParserFactory factory, boo errors = new Vector<>(); this.deep = deep; this.rListener = rListener; - this.blocksToSkipParsing = blocksToSkipParsing; } @Override @@ -129,11 +126,6 @@ private void parseBlock(String root, LinkedHashSet parsers) t continue; } - if (blocksToSkipParsing != null && blocksToSkipParsing.contains(name)) { - this.skipBlock(name); - continue; - } - TransactionParser transaction = factory.getParser(parser); if (transaction == null) { if (deep) { From 15b79b4267ec06a6de2b399a6fd7ec6786643ee9 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Tue, 1 Aug 2023 10:29:43 -0400 Subject: [PATCH 178/402] leave out title if null --- src/main/java/org/javarosa/form/api/FormEntryCaption.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/javarosa/form/api/FormEntryCaption.java b/src/main/java/org/javarosa/form/api/FormEntryCaption.java index 93a4267fbf..9beb6ec7d5 100644 --- a/src/main/java/org/javarosa/form/api/FormEntryCaption.java +++ b/src/main/java/org/javarosa/form/api/FormEntryCaption.java @@ -292,7 +292,7 @@ private String getRepetitionText(String type, FormIndex index, boolean newrep) { } } if (caption == null) { - return title + " " + ix + "/" + count; + return title == null ? ix + "/" + count : title + " " + ix + "/" + count; } Hashtable vars = new Hashtable<>(); From 8af1fb82974b53778df5be5d5a60ba5cfe420772 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 2 Aug 2023 12:51:50 -0500 Subject: [PATCH 179/402] remove stream() APIs as Android is not able to seamlessly support Java8 Apis on older devices --- .../org/commcare/suite/model/StackFrameStep.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/StackFrameStep.java b/src/main/java/org/commcare/suite/model/StackFrameStep.java index 96573031bb..4f8c43ac67 100644 --- a/src/main/java/org/commcare/suite/model/StackFrameStep.java +++ b/src/main/java/org/commcare/suite/model/StackFrameStep.java @@ -26,11 +26,11 @@ import java.io.DataOutputStream; import java.io.IOException; import java.util.Collection; +import java.util.HashMap; import java.util.Hashtable; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.stream.Collectors; /** * @author ctsims @@ -145,10 +145,13 @@ public void initDataInstanceSources(RemoteInstanceFetcher remoteInstanceFetcher) } public Map getInstances(InstanceInitializationFactory iif) { - return dataInstanceSources.values().stream().map((source) -> { - ExternalDataInstance instance = source.toInstance(); - return instance.initialize(iif, source.getInstanceId()); - }).collect(Collectors.toMap(DataInstance::getInstanceId, value -> value)); + HashMap instances = new HashMap<>(); + for (ExternalDataInstanceSource source : dataInstanceSources.values()) { + ExternalDataInstance instance = (ExternalDataInstance)source.toInstance() + .initialize(iif, source.getInstanceId()); + instances.put(instance.getInstanceId(), instance); + } + return instances; } /** From cd8d158a8dd36fb3d01d71123943088ecc513a67 Mon Sep 17 00:00:00 2001 From: Dave Viggiano Date: Mon, 14 Aug 2023 10:56:40 -0400 Subject: [PATCH 180/402] Added TokenAuth subclass of AuthInfo. --- src/main/java/org/commcare/core/network/AuthInfo.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/core/network/AuthInfo.java b/src/main/java/org/commcare/core/network/AuthInfo.java index 7e874eb40e..e60cdfe8c7 100644 --- a/src/main/java/org/commcare/core/network/AuthInfo.java +++ b/src/main/java/org/commcare/core/network/AuthInfo.java @@ -10,6 +10,8 @@ public abstract class AuthInfo { public String password; public boolean wrapDomain; + public String bearerToken; + public static class NoAuth extends AuthInfo { } @@ -24,7 +26,6 @@ public ProvidedAuth(String username, String password, boolean wrapDomain) { this.password = password; this.wrapDomain = wrapDomain; } - } // Auth with the currently-logged in user @@ -32,4 +33,9 @@ public static class CurrentAuth extends AuthInfo { } + public static class TokenAuth extends AuthInfo { + public TokenAuth(String token) { + bearerToken = token; + } + } } From 2ed10c9e7dd570f358d37aa3cd4ab1561139bb47 Mon Sep 17 00:00:00 2001 From: Jonathan Tang <88759246+Jtang-1@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:52:29 -0700 Subject: [PATCH 181/402] Revert "Rc/sccs subtitle" --- .../org/commcare/util/screen/QueryScreen.java | 15 --------------- .../commcare/suite/model/RemoteQueryDatum.java | 10 ++-------- .../java/org/commcare/xml/SessionDatumParser.java | 7 +------ .../suite/model/test/CaseClaimModelTests.java | 3 --- .../resources/case_claim_example/app_strings.txt | 3 +-- src/test/resources/case_claim_example/suite.xml | 5 ----- 6 files changed, 4 insertions(+), 39 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/QueryScreen.java b/src/cli/java/org/commcare/util/screen/QueryScreen.java index 542561c4d0..7eee1ee2e2 100644 --- a/src/cli/java/org/commcare/util/screen/QueryScreen.java +++ b/src/cli/java/org/commcare/util/screen/QueryScreen.java @@ -51,7 +51,6 @@ public class QueryScreen extends Screen { private String[] fields; private String mTitle; private String description; - private String resultsTitle; private String currentMessage; private String domainedUsername; @@ -95,7 +94,6 @@ public void init(SessionWrapper sessionWrapper) throws CommCareSessionException mTitle = getTitleLocaleString(); description = getDescriptionLocaleString(); - resultsTitle = getResultsTitleLocaleString(); } private String getTitleLocaleString() { @@ -116,15 +114,6 @@ private String getDescriptionLocaleString() { return description; } - private String getResultsTitleLocaleString() { - try { - resultsTitle = getQueryDatum().getResultsTitleText().evaluate(); - } catch (NoLocalizedTextException | NullPointerException e) { - resultsTitle = ""; - } - return resultsTitle; - } - private String getTitleLocaleStringLegacy() { try { mTitle = Localization.get("case.search.title"); @@ -248,10 +237,6 @@ public String getDescriptionText() { return description; } - public String getResultsTitle() { - return resultsTitle; - } - @Override public boolean prompt(PrintStream out) { if (doDefaultSearch()) { diff --git a/src/main/java/org/commcare/suite/model/RemoteQueryDatum.java b/src/main/java/org/commcare/suite/model/RemoteQueryDatum.java index 9d25636373..c31b16d7f3 100644 --- a/src/main/java/org/commcare/suite/model/RemoteQueryDatum.java +++ b/src/main/java/org/commcare/suite/model/RemoteQueryDatum.java @@ -32,7 +32,6 @@ public class RemoteQueryDatum extends SessionDatum { private boolean defaultSearch; private Text title; private Text description; - private Text resultsTitle; @SuppressWarnings("unused") public RemoteQueryDatum() { @@ -46,8 +45,7 @@ public RemoteQueryDatum() { public RemoteQueryDatum(URL url, String storageInstance, List hiddenQueryValues, OrderedHashtable userQueryPrompts, - boolean useCaseTemplate, boolean defaultSearch, - Text title, Text description, Text resultsTitle) { + boolean useCaseTemplate, boolean defaultSearch, Text title, Text description) { super(storageInstance, url.toString()); this.hiddenQueryValues = hiddenQueryValues; this.userQueryPrompts = userQueryPrompts; @@ -55,7 +53,6 @@ public RemoteQueryDatum(URL url, String storageInstance, this.defaultSearch = defaultSearch; this.title = title; this.description = description; - this.resultsTitle = resultsTitle; } public OrderedHashtable getUserQueryPrompts() { @@ -92,8 +89,6 @@ public Text getDescriptionText() { return description; } - public Text getResultsTitleText() { return resultsTitle; } - @Override public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { @@ -107,7 +102,6 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) description = (Text) ExtUtil.read(in, new ExtWrapNullable(Text.class), pf); useCaseTemplate = ExtUtil.readBool(in); defaultSearch = ExtUtil.readBool(in); - resultsTitle = (Text) ExtUtil.read(in, new ExtWrapNullable(Text.class), pf); } @@ -120,6 +114,6 @@ public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.write(out, new ExtWrapNullable(description)); ExtUtil.writeBool(out, useCaseTemplate); ExtUtil.writeBool(out, defaultSearch); - ExtUtil.write(out, new ExtWrapNullable(resultsTitle)); + } } diff --git a/src/main/java/org/commcare/xml/SessionDatumParser.java b/src/main/java/org/commcare/xml/SessionDatumParser.java index e26b436ebd..9dcb161fb8 100644 --- a/src/main/java/org/commcare/xml/SessionDatumParser.java +++ b/src/main/java/org/commcare/xml/SessionDatumParser.java @@ -128,7 +128,6 @@ private RemoteQueryDatum parseRemoteQueryDatum() boolean defaultSearch = "true".equals(parser.getAttributeValue(null, "default_search")); Text title = null; Text description = null; - Text resultsTitle = null; ArrayList hiddenQueryValues = new ArrayList(); while (nextTagInBlock("query")) { String tagName = parser.getName(); @@ -143,13 +142,9 @@ private RemoteQueryDatum parseRemoteQueryDatum() } else if ("description".equals(tagName)) { nextTagInBlock("description"); description = new TextParser(parser).parse(); - } else if ("results-title".equals(tagName)) { - nextTagInBlock("results-title"); - resultsTitle = new TextParser(parser).parse(); } } return new RemoteQueryDatum(queryUrl, queryResultStorageInstance, - hiddenQueryValues, userQueryPrompts, useCaseTemplate, defaultSearch, title, description, - resultsTitle); + hiddenQueryValues, userQueryPrompts, useCaseTemplate, defaultSearch, title, description); } } diff --git a/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java b/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java index 0bb9f408f7..53f597b368 100644 --- a/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java +++ b/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java @@ -62,9 +62,6 @@ public void testRemoteQueryDatum() throws Exception { Text description = ((RemoteQueryDatum) datum).getDescriptionText(); Assert.assertEquals("Description text", description.evaluate()); - - Text resultsTitle = ((RemoteQueryDatum) datum).getResultsTitleText(); - Assert.assertEquals("Results Title", resultsTitle.evaluate()); } @Test diff --git a/src/test/resources/case_claim_example/app_strings.txt b/src/test/resources/case_claim_example/app_strings.txt index 5768caf89f..0c06c4a2dd 100644 --- a/src/test/resources/case_claim_example/app_strings.txt +++ b/src/test/resources/case_claim_example/app_strings.txt @@ -6,5 +6,4 @@ query.another_age.validation.message=another age should be greater than 18 query.age.required.message=One of age or DOB is required query.dob.required.message=One of age or DOB is required query.title=Title Label -query.description=Description text -query.resultsTitle=Results Title \ No newline at end of file +query.description=Description text \ No newline at end of file diff --git a/src/test/resources/case_claim_example/suite.xml b/src/test/resources/case_claim_example/suite.xml index 2336f8c6ca..ee0ba9c4ac 100644 --- a/src/test/resources/case_claim_example/suite.xml +++ b/src/test/resources/case_claim_example/suite.xml @@ -72,11 +72,6 @@ - - - - - From 675486927387e115f9ac73e0d4ff8ab6d6dd44e8 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 17 Jul 2023 15:27:56 -0500 Subject: [PATCH 182/402] Adds model for EndpointArgument --- .../suite/model/EndpointArgument.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/main/java/org/commcare/suite/model/EndpointArgument.java diff --git a/src/main/java/org/commcare/suite/model/EndpointArgument.java b/src/main/java/org/commcare/suite/model/EndpointArgument.java new file mode 100644 index 0000000000..e4308c8557 --- /dev/null +++ b/src/main/java/org/commcare/suite/model/EndpointArgument.java @@ -0,0 +1,62 @@ +package org.commcare.suite.model; + +import org.javarosa.core.util.externalizable.DeserializationException; +import org.javarosa.core.util.externalizable.ExtUtil; +import org.javarosa.core.util.externalizable.Externalizable; +import org.javarosa.core.util.externalizable.PrototypeFactory; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import javax.annotation.Nullable; + +public class EndpointArgument implements Externalizable { + + private String id; + + @Nullable + private String instanceId; + + @Nullable + private String instanceSrc; + + // for serialization + public EndpointArgument() { + } + + public EndpointArgument(String id, @Nullable String instanceId, @Nullable String instanceSrc) { + this.id = id; + this.instanceId = instanceId; + this.instanceSrc = instanceSrc; + } + + @Override + public void readExternal(DataInputStream in, PrototypeFactory pf) + throws IOException, DeserializationException { + id = ExtUtil.readString(in); + instanceId = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); + instanceSrc = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); + } + + @Override + public void writeExternal(DataOutputStream out) throws IOException { + ExtUtil.writeString(out, id); + ExtUtil.writeString(out, ExtUtil.emptyIfNull(instanceId)); + ExtUtil.writeString(out, ExtUtil.emptyIfNull(instanceSrc)); + } + + public String getId() { + return id; + } + + @Nullable + public String getInstanceId() { + return instanceId; + } + + @Nullable + public String getInstanceSrc() { + return instanceSrc; + } +} From 23a2eee9a2a8cd10fea9829276613d6da786cc61 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 17 Jul 2023 16:02:33 -0500 Subject: [PATCH 183/402] Parse instance argument for endpoint --- .../java/org/commcare/xml/EndpointParser.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/commcare/xml/EndpointParser.java b/src/main/java/org/commcare/xml/EndpointParser.java index 428e720b3c..843b06612d 100644 --- a/src/main/java/org/commcare/xml/EndpointParser.java +++ b/src/main/java/org/commcare/xml/EndpointParser.java @@ -1,6 +1,7 @@ package org.commcare.xml; import org.commcare.suite.model.Endpoint; +import org.commcare.suite.model.EndpointArgument; import org.commcare.suite.model.StackOperation; import org.javarosa.xml.ElementParser; import org.javarosa.xml.util.InvalidStructureException; @@ -17,6 +18,9 @@ public class EndpointParser extends ElementParser { static final String NAME_ENDPOINT = "endpoint"; private static final String NAME_ARGUMENT = "argument"; + private static final String ATTR_ARGUMENT_ID = "id"; + private static final String ATTR_ARGUMENT_INSTANCE_ID = "instance-id"; + private static final String ATTR_ARGUMENT_INSTANCE_SRC = "instance-src"; public EndpointParser(KXmlParser parser) { @@ -31,16 +35,23 @@ public Endpoint parse() throws InvalidStructureException, IOException, XmlPullPa } Vector stackOperations = new Vector<>(); - Vector arguments = new Vector<>(); + Vector arguments = new Vector<>(); while (nextTagInBlock(NAME_ENDPOINT)) { String tagName = parser.getName().toLowerCase(); if (tagName.contentEquals(NAME_ARGUMENT)) { - String argumentID = parser.getAttributeValue(null, "id"); + String argumentID = parser.getAttributeValue(null, ATTR_ARGUMENT_ID); if (argumentID == null || argumentID.isEmpty()) { throw new InvalidStructureException("argument must define a non empty id", parser); } - arguments.add(argumentID); + String argInstanceId = parser.getAttributeValue(null, ATTR_ARGUMENT_INSTANCE_ID); + String argInstanceSrc = parser.getAttributeValue(null, ATTR_ARGUMENT_INSTANCE_SRC); + + if (argInstanceId != null && argInstanceSrc == null) { + throw new InvalidStructureException( + "argument containing a non-null instance-id must define an instance-src", parser); + } + arguments.add(new EndpointArgument(argumentID, argInstanceId, argInstanceSrc)); } else if (tagName.contentEquals(NAME_STACK)) { StackOpParser sop = new StackOpParser(parser); while (this.nextTagInBlock(NAME_STACK)) { From 522fc466e8eb7d8fc8cb4b2543474a4451bf7a9f Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 17 Jul 2023 16:06:21 -0500 Subject: [PATCH 184/402] Handle EndpointArguments in Endpoint --- .../org/commcare/suite/model/Endpoint.java | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/Endpoint.java b/src/main/java/org/commcare/suite/model/Endpoint.java index be70e1fb0e..65e468bcc0 100644 --- a/src/main/java/org/commcare/suite/model/Endpoint.java +++ b/src/main/java/org/commcare/suite/model/Endpoint.java @@ -12,19 +12,20 @@ import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.Set; import java.util.Vector; public class Endpoint implements Externalizable { String id; - Vector arguments; + Vector arguments; Vector stackOperations; // for serialization public Endpoint() { } - public Endpoint(String id, Vector arguments, Vector stackOperations) { + public Endpoint(String id, Vector arguments, Vector stackOperations) { this.id = id; this.arguments = arguments; this.stackOperations = stackOperations; @@ -33,7 +34,7 @@ public Endpoint(String id, Vector arguments, Vector stac @Override public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { id = ExtUtil.readString(in); - arguments = (Vector)ExtUtil.read(in, new ExtWrapList(String.class), pf); + arguments = (Vector)ExtUtil.read(in, new ExtWrapList(EndpointArgument.class), pf); stackOperations = (Vector)ExtUtil.read(in, new ExtWrapList(StackOperation.class), pf); } @@ -48,7 +49,7 @@ public String getId() { return id; } - public Vector getArguments() { + public Vector getArguments() { return arguments; } @@ -59,40 +60,60 @@ public Vector getStackOperations() { // Utility Functions public static void populateEndpointArgumentsToEvaluationContext(Endpoint endpoint, ArrayList args, EvaluationContext evaluationContext) { - Vector endpointArguments = endpoint.getArguments(); + Vector endpointArguments = endpoint.getArguments(); if (endpointArguments.size() > args.size()) { - Vector missingArguments = new Vector(endpointArguments.subList(args.size(), endpointArguments.size())); + Vector missingArguments = new Vector<>(); + for (int i = args.size(); i < endpointArguments.size(); i++) { + missingArguments.add(endpointArguments.get(i).getId()); + } throw new InvalidEndpointArgumentsException(missingArguments, null); } for (int i = 0; i < endpointArguments.size(); i++) { - String argumentName = endpointArguments.elementAt(i); + String argumentName = endpointArguments.elementAt(i).getId(); evaluationContext.setVariable(argumentName, args.get(i)); } } public static void populateEndpointArgumentsToEvaluationContext(Endpoint endpoint, HashMap args, EvaluationContext evaluationContext) { - Vector endpointArguments = endpoint.getArguments(); - - Vector missingArguments = (Vector)endpointArguments.clone(); - missingArguments.removeAll(args.keySet()); + Vector endpointArguments = endpoint.getArguments(); + Set argumentIds = args.keySet(); + Vector missingArguments = new Vector<>(); + for (EndpointArgument endpointArgument : endpointArguments) { + if(!argumentIds.contains(endpointArgument.getId())){ + missingArguments.add(endpointArgument.getId()); + } + } - Vector unexpectedArguments = new Vector(args.keySet()); - unexpectedArguments.removeAll(endpointArguments); + Vector unexpectedArguments = new Vector<>(); + for (String argumentId : argumentIds) { + if(!isValidArgumentId(endpointArguments, argumentId)){ + unexpectedArguments.add(argumentId); + } + } if (missingArguments.size() > 0 || unexpectedArguments.size() > 0) { throw new InvalidEndpointArgumentsException(missingArguments, unexpectedArguments); } for (int i = 0; i < endpointArguments.size(); i++) { - String argumentName = endpointArguments.elementAt(i); + String argumentName = endpointArguments.elementAt(i).getId(); if (args.containsKey(argumentName)) { evaluationContext.setVariable(argumentName, args.get(argumentName)); } } } + private static boolean isValidArgumentId(Vector endpointArguments, String argumentId) { + for (EndpointArgument endpointArgument : endpointArguments) { + if (endpointArgument.getId().contentEquals(argumentId)) { + return true; + } + } + return false; + } + public static class InvalidEndpointArgumentsException extends RuntimeException { private final Vector missingArguments; private final Vector unexpectedArguments; From 294247fcada688084df60238edf0f3950cf8a6de Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 18 Jul 2023 16:18:15 -0500 Subject: [PATCH 185/402] validate instance-src at parse time --- src/main/java/org/commcare/xml/EndpointParser.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/xml/EndpointParser.java b/src/main/java/org/commcare/xml/EndpointParser.java index 843b06612d..f0b4d83bf5 100644 --- a/src/main/java/org/commcare/xml/EndpointParser.java +++ b/src/main/java/org/commcare/xml/EndpointParser.java @@ -13,6 +13,9 @@ import java.util.Vector; import static org.commcare.xml.StackOpParser.NAME_STACK; +import static org.javarosa.core.model.instance.ExternalDataInstance.JR_SELECTED_ENTITIES_REFERENCE; + +import com.google.common.collect.ImmutableList; public class EndpointParser extends ElementParser { @@ -49,8 +52,15 @@ public Endpoint parse() throws InvalidStructureException, IOException, XmlPullPa if (argInstanceId != null && argInstanceSrc == null) { throw new InvalidStructureException( - "argument containing a non-null instance-id must define an instance-src", parser); + "Endpoint argument containing a non-null instance-id must define an instance-src", parser); } + + ImmutableList validInstanceSrc = ImmutableList.of(JR_SELECTED_ENTITIES_REFERENCE); + if (!validInstanceSrc.contains(argInstanceSrc)) { + throw new InvalidStructureException( + "instance-src for an endpoint argument must be one of " + validInstanceSrc, parser); + } + arguments.add(new EndpointArgument(argumentID, argInstanceId, argInstanceSrc)); } else if (tagName.contentEquals(NAME_STACK)) { StackOpParser sop = new StackOpParser(parser); From d36c796962adc61cd6583cad9a01d9cdfe25c956 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 18 Jul 2023 16:26:48 -0500 Subject: [PATCH 186/402] Adds isInstanceArgument --- src/main/java/org/commcare/suite/model/EndpointArgument.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/commcare/suite/model/EndpointArgument.java b/src/main/java/org/commcare/suite/model/EndpointArgument.java index e4308c8557..ab25447f8a 100644 --- a/src/main/java/org/commcare/suite/model/EndpointArgument.java +++ b/src/main/java/org/commcare/suite/model/EndpointArgument.java @@ -59,4 +59,8 @@ public String getInstanceId() { public String getInstanceSrc() { return instanceSrc; } + + public boolean isInstanceArgument() { + return instanceId != null; + } } From 6a171e18f79d96c288e703456513b5a98fe0b8f4 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 18 Jul 2023 16:31:23 -0500 Subject: [PATCH 187/402] Java docs --- .../java/org/commcare/suite/model/EndpointArgument.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/org/commcare/suite/model/EndpointArgument.java b/src/main/java/org/commcare/suite/model/EndpointArgument.java index ab25447f8a..8bbbe81d1b 100644 --- a/src/main/java/org/commcare/suite/model/EndpointArgument.java +++ b/src/main/java/org/commcare/suite/model/EndpointArgument.java @@ -11,6 +11,9 @@ import javax.annotation.Nullable; +/** + * Model class to represent an argument to Endpoint + */ public class EndpointArgument implements Externalizable { private String id; @@ -60,6 +63,11 @@ public String getInstanceSrc() { return instanceSrc; } + /** + * If the argument should be processed into a external data instance + * + * @return true if the argument defines instance attributes, false otherwise + */ public boolean isInstanceArgument() { return instanceId != null; } From 45a855de55804c8a2fae6c556be56d5376dc6bc0 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 19 Jul 2023 09:03:00 -0500 Subject: [PATCH 188/402] validate instance has valid cases --- .../util/screen/MultiSelectEntityScreen.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java index e7b774ab90..d69985d8f4 100644 --- a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java @@ -134,9 +134,18 @@ private void prcessSelectionAsGuid(String guid) throws CommCareSessionException "Could not make selection with reference id " + guid + " on this screen. " + " If this error persists please report a bug to CommCareHQ."); } + validateEntitiesInInstance(cachedInstance); storageReferenceId = guid; } + private void validateEntitiesInInstance(ExternalDataInstance instance) throws CommCareSessionException { + AbstractTreeElement root = instance.getRoot(); + for (int i = 0; i < root.getNumChildren(); i++) { + String entityVal = root.getChildAt(i).getValue().uncast().getString(); + getAndValidateEntityReference(entityVal); + } + } + private String getNeededDatumId() { return getSession().getNeededDatum().getDataId(); } @@ -156,18 +165,23 @@ private void processSelectedValues(String[] selectedValues) if (selectedValues != null && validateSelectionSize(selectedValues.length)) { String[] evaluatedValues = new String[selectedValues.length]; for (int i = 0; i < selectedValues.length; i++) { - TreeReference currentReference = getEntityReference(selectedValues[i]); - if (currentReference == null) { - throw new CommCareSessionException( - "Could not select case " + selectedValues[i] + " on this screen. " + - " If this error persists please report a bug to CommCareHQ."); - } + TreeReference currentReference = getAndValidateEntityReference(selectedValues[i]); evaluatedValues[i] = getReturnValueFromSelection(currentReference); } processSelectionIntoInstance(evaluatedValues); } } + private TreeReference getAndValidateEntityReference(String selectedValue) throws CommCareSessionException { + TreeReference currentReference = getEntityReference(selectedValue); + if (currentReference == null) { + throw new CommCareSessionException( + "Could not select case " + selectedValue + " on this screen. " + + " If this error persists please report a bug to CommCareHQ."); + } + return currentReference; + } + private void processSelectionIntoInstance(String[] evaluatedValues) { ExternalDataInstance instance = VirtualInstances.buildSelectedValuesInstance( getSession().getNeededDatum().getDataId(), From 68ab31b480c540e912c2983afddba24213310246 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 19 Jul 2023 15:23:37 -0500 Subject: [PATCH 189/402] Adds endpoint to app structure tests --- src/test/resources/app_structure/suite.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/test/resources/app_structure/suite.xml b/src/test/resources/app_structure/suite.xml index e104dd4df9..d35734161e 100644 --- a/src/test/resources/app_structure/suite.xml +++ b/src/test/resources/app_structure/suite.xml @@ -230,5 +230,13 @@ Menu - + + + + + + + + + From 9a89dad9603145884fcbc78a28073587a972101d Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 19 Jul 2023 15:34:33 -0500 Subject: [PATCH 190/402] only check for valid instance-src when non-null --- src/main/java/org/commcare/xml/EndpointParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/xml/EndpointParser.java b/src/main/java/org/commcare/xml/EndpointParser.java index f0b4d83bf5..d5e3f0c9a1 100644 --- a/src/main/java/org/commcare/xml/EndpointParser.java +++ b/src/main/java/org/commcare/xml/EndpointParser.java @@ -56,7 +56,7 @@ public Endpoint parse() throws InvalidStructureException, IOException, XmlPullPa } ImmutableList validInstanceSrc = ImmutableList.of(JR_SELECTED_ENTITIES_REFERENCE); - if (!validInstanceSrc.contains(argInstanceSrc)) { + if (argInstanceSrc != null && !validInstanceSrc.contains(argInstanceSrc)) { throw new InvalidStructureException( "instance-src for an endpoint argument must be one of " + validInstanceSrc, parser); } From c5086b7c4ef95ed9883e6c2c942c2cdf0cb0f4be Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 9 Aug 2023 10:56:09 -0500 Subject: [PATCH 191/402] remove unused constructor --- .../core/process/CommCareInstanceInitializer.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java b/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java index 7c15d4644a..cc5cc37c6f 100644 --- a/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java +++ b/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java @@ -54,14 +54,6 @@ public CommCareInstanceInitializer(UserSandbox sandbox) { this(null, sandbox, null); } - public CommCareInstanceInitializer(UserSandbox sandbox, CommCarePlatform platform) { - this(null, sandbox, platform); - } - - public CommCareInstanceInitializer(UserSandbox sandbox, CommCareSession session) { - this(session, sandbox, null); - } - public CommCareInstanceInitializer(CommCareSession session, UserSandbox sandbox, CommCarePlatform platform) { this.session = session; this.mSandbox = sandbox; From 4880a84783e7b1222e02c76b91263e373d70731a Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 9 Aug 2023 10:59:57 -0500 Subject: [PATCH 192/402] pass SessionWrapper instead of Session to Instance initializer we already do so, this just makes it more apparent that we are passing SessionWrapper --- .../core/process/CommCareInstanceInitializer.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java b/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java index cc5cc37c6f..5aa05a2d63 100644 --- a/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java +++ b/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java @@ -9,7 +9,7 @@ import org.commcare.core.interfaces.UserSandbox; import org.commcare.core.sandbox.SandboxUtils; import org.commcare.data.xml.VirtualInstances; -import org.commcare.session.CommCareSession; +import org.commcare.modern.session.SessionWrapper; import org.commcare.session.SessionFrame; import org.commcare.session.SessionInstanceBuilder; import org.commcare.suite.model.StackFrameStep; @@ -37,7 +37,7 @@ */ public class CommCareInstanceInitializer extends InstanceInitializationFactory { - protected final CommCareSession session; + protected final SessionWrapper sessionWrapper; protected CaseInstanceTreeElement casebase; protected LedgerInstanceTreeElement stockbase; private final LocalCacheTable fixtureBases = new LocalCacheTable<>(); @@ -54,8 +54,8 @@ public CommCareInstanceInitializer(UserSandbox sandbox) { this(null, sandbox, null); } - public CommCareInstanceInitializer(CommCareSession session, UserSandbox sandbox, CommCarePlatform platform) { - this.session = session; + public CommCareInstanceInitializer(SessionWrapper sessionWrapper, UserSandbox sandbox, CommCarePlatform platform) { + this.sessionWrapper = sessionWrapper; this.mSandbox = sandbox; this.mPlatform = platform; } @@ -208,7 +208,7 @@ protected InstanceRoot setupSessionData(ExternalDataInstance instance) { } User u = mSandbox.getLoggedInUserUnsafe(); TreeElement root = - SessionInstanceBuilder.getSessionInstance(session.getFrame(), getDeviceId(), + SessionInstanceBuilder.getSessionInstance(sessionWrapper.getFrame(), getDeviceId(), getVersionString(), getCurrentDrift(), u.getUsername(), u.getUniqueId(), u.getProperties()); root.setParent(instance.getBase()); @@ -220,7 +220,7 @@ protected long getCurrentDrift() { } protected InstanceRoot getExternalDataInstanceSource(String reference, String stepType) { - for (StackFrameStep step : session.getFrame().getSteps()) { + for (StackFrameStep step : sessionWrapper.getFrame().getSteps()) { if (step.getType().equals(stepType) && step.hasDataInstanceSource(reference)) { return step.getDataInstanceSource(reference); } @@ -232,7 +232,7 @@ protected InstanceRoot getExternalDataInstanceSource(String reference, String st * Required for legacy instance support */ protected InstanceRoot getExternalDataInstanceSourceById(String instanceId, String stepType) { - for (StackFrameStep step : session.getFrame().getSteps()) { + for (StackFrameStep step : sessionWrapper.getFrame().getSteps()) { if (step.getType().equals(stepType)) { ExternalDataInstanceSource source = step.getDataInstanceSourceById(instanceId); if (source != null) { From 7e41f595de2aba5d3a6befab24a940a5488ce861 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 10 Aug 2023 00:01:53 -0500 Subject: [PATCH 193/402] Makes instance fetcher a class property of session wrapper --- .../modern/session/SessionWrapper.java | 24 ++++++++++++++++--- .../session/SessionWrapperInterface.java | 2 +- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/commcare/modern/session/SessionWrapper.java b/src/main/java/org/commcare/modern/session/SessionWrapper.java index af6b0c3033..f04aaee86a 100644 --- a/src/main/java/org/commcare/modern/session/SessionWrapper.java +++ b/src/main/java/org/commcare/modern/session/SessionWrapper.java @@ -23,12 +23,19 @@ public class SessionWrapper extends CommCareSession implements SessionWrapperInt final protected UserSandbox mSandbox; final protected CommCarePlatform mPlatform; protected CommCareInstanceInitializer initializer; + protected RemoteInstanceFetcher remoteInstanceFetcher; - public SessionWrapper(CommCareSession session, CommCarePlatform platform, UserSandbox sandbox) { - this(platform, sandbox); + public SessionWrapper(CommCareSession session, CommCarePlatform platform, UserSandbox sandbox, + RemoteInstanceFetcher remoteInstanceFetcher) { + this(platform, sandbox, remoteInstanceFetcher); this.frame = session.getFrame(); this.setFrameStack(session.getFrameStack()); } + + + public SessionWrapper(CommCareSession session, CommCarePlatform platform, UserSandbox sandbox) { + this(session, platform, sandbox, null); + } public SessionWrapper(CommCarePlatform platform, UserSandbox sandbox) { super(platform); @@ -36,6 +43,13 @@ public SessionWrapper(CommCarePlatform platform, UserSandbox sandbox) { this.mPlatform = platform; } + public SessionWrapper(CommCarePlatform platform, UserSandbox sandbox, RemoteInstanceFetcher remoteInstanceFetcher) { + super(platform); + this.mSandbox = sandbox; + this.mPlatform = platform; + this.remoteInstanceFetcher = remoteInstanceFetcher; + } + /** * @return The evaluation context for the current state. */ @@ -72,7 +86,7 @@ public CommCareInstanceInitializer getIIF() { return initializer; } - public void prepareExternalSources(RemoteInstanceFetcher remoteInstanceFetcher) throws RemoteInstanceFetcher.RemoteInstanceException { + public void prepareExternalSources() throws RemoteInstanceFetcher.RemoteInstanceException { for(StackFrameStep step : frame.getSteps()) { step.initDataInstanceSources(remoteInstanceFetcher); } @@ -101,4 +115,8 @@ public String getNeededData() { public void stepBack() { super.stepBack(getEvaluationContext()); } + + public RemoteInstanceFetcher getRemoteInstanceFetcher() { + return remoteInstanceFetcher; + } } diff --git a/src/main/java/org/commcare/modern/session/SessionWrapperInterface.java b/src/main/java/org/commcare/modern/session/SessionWrapperInterface.java index dcf3cefe80..0ed9b1556d 100644 --- a/src/main/java/org/commcare/modern/session/SessionWrapperInterface.java +++ b/src/main/java/org/commcare/modern/session/SessionWrapperInterface.java @@ -22,5 +22,5 @@ public interface SessionWrapperInterface { EvaluationContext getRestrictedEvaluationContext(String commandId, Set instancesToInclude); EvaluationContext getEvaluationContextWithAccumulatedInstances(String commandID, XPathAnalyzable xPathAnalyzable); - void prepareExternalSources(RemoteInstanceFetcher fetcher) throws RemoteInstanceFetcher.RemoteInstanceException; + void prepareExternalSources() throws RemoteInstanceFetcher.RemoteInstanceException; } From 3431fd7768dd0010c462d1bde5c8a15445a749af Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 10 Aug 2023 00:04:24 -0500 Subject: [PATCH 194/402] Adds interface method --- .../org/commcare/core/interfaces/RemoteInstanceFetcher.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/commcare/core/interfaces/RemoteInstanceFetcher.java b/src/main/java/org/commcare/core/interfaces/RemoteInstanceFetcher.java index ef62900a60..1c96c73673 100644 --- a/src/main/java/org/commcare/core/interfaces/RemoteInstanceFetcher.java +++ b/src/main/java/org/commcare/core/interfaces/RemoteInstanceFetcher.java @@ -12,6 +12,8 @@ public interface RemoteInstanceFetcher { AbstractTreeElement getExternalRoot(String instanceId, ExternalDataInstanceSource source, String refId) throws RemoteInstanceException; + VirtualDataInstanceStorage getVirtualDataInstanceStorage(); + class RemoteInstanceException extends Exception { public RemoteInstanceException(String message) { From 4aeea3b1ab0a4af2c89a026521f75d428dbfaac0 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 10 Aug 2023 00:05:31 -0500 Subject: [PATCH 195/402] utility method to store instance, move instance not found exception to core from FP --- .../util/screen/MultiSelectEntityScreen.java | 23 +++++------ .../commcare/data/xml/VirtualInstances.java | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java index d69985d8f4..bce2b3cbcd 100644 --- a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java @@ -9,6 +9,7 @@ import org.commcare.core.interfaces.VirtualDataInstanceStorage; import org.commcare.data.xml.VirtualInstances; import org.commcare.modern.session.SessionWrapper; +import org.commcare.modern.util.Pair; import org.commcare.session.CommCareSession; import org.commcare.suite.model.MultiSelectEntityDatum; import org.commcare.util.FormDataUtil; @@ -80,7 +81,7 @@ public boolean autoSelectEntities(SessionWrapper session) { for (int i = 0; i < selectionSize; i++) { evaluatedValues[i] = getReturnValueFromSelection(references.elementAt(i)); } - processSelectionIntoInstance(evaluatedValues); + processSelectionIntoInstance(evaluatedValues, getNeededDatumId()); updateSession(session); return true; } @@ -156,7 +157,7 @@ private void processSelectedReferences(TreeReference[] selectedRefs) { for (int i = 0; i < selectedRefs.length; i++) { evaluatedValues[i] = getReturnValueFromSelection(selectedRefs[i]); } - processSelectionIntoInstance(evaluatedValues); + processSelectionIntoInstance(evaluatedValues, getNeededDatumId()); } } @@ -168,7 +169,7 @@ private void processSelectedValues(String[] selectedValues) TreeReference currentReference = getAndValidateEntityReference(selectedValues[i]); evaluatedValues[i] = getReturnValueFromSelection(currentReference); } - processSelectionIntoInstance(evaluatedValues); + processSelectionIntoInstance(evaluatedValues, getNeededDatumId()); } } @@ -182,17 +183,11 @@ private TreeReference getAndValidateEntityReference(String selectedValue) throws return currentReference; } - private void processSelectionIntoInstance(String[] evaluatedValues) { - ExternalDataInstance instance = VirtualInstances.buildSelectedValuesInstance( - getSession().getNeededDatum().getDataId(), - evaluatedValues); - String guid = virtualDataInstanceStorage.write(instance); - storageReferenceId = guid; - - // rebuild instance with the source - ExternalDataInstanceSource instanceSource = ExternalDataInstanceSource.buildVirtual(instance, - storageReferenceId); - selectedValuesInstance = instanceSource.toInstance(); + private void processSelectionIntoInstance(String[] evaluatedValues, String instanceId) { + Pair guidAndInstance = VirtualInstances.storeSelectedValuesInInstance( + virtualDataInstanceStorage, evaluatedValues, instanceId); + storageReferenceId = guidAndInstance.first; + selectedValuesInstance = guidAndInstance.second; } @Override diff --git a/src/main/java/org/commcare/data/xml/VirtualInstances.java b/src/main/java/org/commcare/data/xml/VirtualInstances.java index 6427078d2e..661869a507 100644 --- a/src/main/java/org/commcare/data/xml/VirtualInstances.java +++ b/src/main/java/org/commcare/data/xml/VirtualInstances.java @@ -6,7 +6,10 @@ import com.google.common.collect.ImmutableMap; +import org.commcare.core.interfaces.VirtualDataInstanceStorage; +import org.commcare.modern.util.Pair; import org.javarosa.core.model.instance.ExternalDataInstance; +import org.javarosa.core.model.instance.ExternalDataInstanceSource; import org.javarosa.core.model.instance.TreeElement; import java.util.ArrayList; @@ -49,6 +52,27 @@ public static ExternalDataInstance buildSelectedValuesInstance( return new ExternalDataInstance(getSelectedEntitiesReference(instanceId), instanceId, root); } + /** + * Builds and stores the selected entitied into selected entities instance + * + * @param virtualDataInstanceStorage Instance Storage + * @param selectedValues Values to be stored into instance + * @param instanceId instance id for the new instance + * @return A pair of unique storage id for the instance and the newly generated instance + */ + public static Pair storeSelectedValuesInInstance( + VirtualDataInstanceStorage virtualDataInstanceStorage, String[] selectedValues, String instanceId) { + ExternalDataInstance instance = VirtualInstances.buildSelectedValuesInstance( + instanceId, + selectedValues); + String guid = virtualDataInstanceStorage.write(instance); + + // rebuild instance with the source + ExternalDataInstanceSource instanceSource = ExternalDataInstanceSource.buildVirtual(instance, guid); + ExternalDataInstance selectedValuesInstance = instanceSource.toInstance(); + return new Pair<>(guid, selectedValuesInstance); + } + public static String getSelectedEntitiesReference(String referenceId) { return getInstanceReference(JR_SELECTED_ENTITIES_REFERENCE, referenceId); @@ -92,4 +116,18 @@ public static String getReferenceScheme(String reference) { public static String getInstanceReference(String referenceScheme, String referenceId) { return referenceScheme.concat("/").concat(referenceId); } + + /** + * Throw when the data instance with the given key doesn't exist in the DB + */ + public static class InstanceNotFoundException extends RuntimeException { + + public InstanceNotFoundException(String key, String namespace) { + super(String.format( + "Could not find data instance with ID %s (namespace=%s)." + + "Redirecting to home screen. If this issue persists, please file a bug report.", + key, namespace + )); + } + } } From 4d140b6ea92147a730fe9e9f7ad95fa1e598c0c8 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 10 Aug 2023 00:07:02 -0500 Subject: [PATCH 196/402] directly look for instance-datum value in storage for initing selected entities instance --- .../process/CommCareInstanceInitializer.java | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java b/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java index 5aa05a2d63..ffacb76cee 100644 --- a/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java +++ b/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java @@ -6,7 +6,9 @@ import org.commcare.cases.instance.CaseDataInstance; import org.commcare.cases.instance.CaseInstanceTreeElement; import org.commcare.cases.instance.LedgerInstanceTreeElement; +import org.commcare.core.interfaces.RemoteInstanceFetcher; import org.commcare.core.interfaces.UserSandbox; +import org.commcare.core.interfaces.VirtualDataInstanceStorage; import org.commcare.core.sandbox.SandboxUtils; import org.commcare.data.xml.VirtualInstances; import org.commcare.modern.session.SessionWrapper; @@ -54,7 +56,8 @@ public CommCareInstanceInitializer(UserSandbox sandbox) { this(null, sandbox, null); } - public CommCareInstanceInitializer(SessionWrapper sessionWrapper, UserSandbox sandbox, CommCarePlatform platform) { + public CommCareInstanceInitializer(SessionWrapper sessionWrapper, UserSandbox sandbox, + CommCarePlatform platform) { this.sessionWrapper = sessionWrapper; this.mSandbox = sandbox; this.mPlatform = platform; @@ -84,13 +87,44 @@ public InstanceRoot generateRoot(ExternalDataInstance instance) { } else if (ref.startsWith(ExternalDataInstance.JR_REMOTE_REFERENCE)) { return setupExternalDataInstance(instance, ref, SessionFrame.STATE_QUERY_REQUEST); } else if (ref.startsWith(JR_SELECTED_ENTITIES_REFERENCE)) { - return setupExternalDataInstance(instance, ref, SessionFrame.STATE_MULTIPLE_DATUM_VAL); + return setupSelectedEntitiesInstance(instance, ref); } else if (ref.startsWith(JR_SEARCH_INPUT_REFERENCE)) { return setupExternalDataInstance(instance, ref, SessionFrame.STATE_QUERY_REQUEST); } return ConcreteInstanceRoot.NULL; } + private InstanceRoot setupSelectedEntitiesInstance(ExternalDataInstance instance, String ref) { + String stepType = SessionFrame.STATE_MULTIPLE_DATUM_VAL; + InstanceRoot instanceRoot = setupExternalDataInstance(instance, ref, stepType); + if (instanceRoot == ConcreteInstanceRoot.NULL) { + instanceRoot = getExternalDataInstanceSourceByStepValue(instance, stepType); + } + return instanceRoot; + } + + // Tries to get instance by looking for the instance with id equal to the datum value in the storage + private InstanceRoot getExternalDataInstanceSourceByStepValue(ExternalDataInstance instance, + String stepType) { + RemoteInstanceFetcher instanceFetcher = sessionWrapper.getRemoteInstanceFetcher(); + if (instanceFetcher != null) { + VirtualDataInstanceStorage instanceStorage = instanceFetcher.getVirtualDataInstanceStorage(); + for (StackFrameStep step : sessionWrapper.getFrame().getSteps()) { + if (step.getType().equals(stepType)) { + try { + ExternalDataInstance loadedInstance = instanceStorage.read(step.getValue(), + instance.getInstanceId(), + instance.getReference()); + return new ConcreteInstanceRoot(loadedInstance.getRoot()); + } catch (VirtualInstances.InstanceNotFoundException e) { + // continue looping + } + } + } + } + return ConcreteInstanceRoot.NULL; + } + /** * Initialises instances with reference to 'selected_cases' * From 73f1aa13d02ba383d783391e0ec754f93821c9f0 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 10 Aug 2023 10:14:35 -0500 Subject: [PATCH 197/402] Adds parsing for instance-datums in stack operations --- src/main/java/org/commcare/suite/model/StackFrameStep.java | 2 ++ src/main/java/org/commcare/xml/StackFrameStepParser.java | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/suite/model/StackFrameStep.java b/src/main/java/org/commcare/suite/model/StackFrameStep.java index 4f8c43ac67..1a38ec75e1 100644 --- a/src/main/java/org/commcare/suite/model/StackFrameStep.java +++ b/src/main/java/org/commcare/suite/model/StackFrameStep.java @@ -200,6 +200,8 @@ public StackFrameStep defineStep(EvaluationContext ec, SessionDatum neededDatum) switch (elementType) { case SessionFrame.STATE_DATUM_VAL: return new StackFrameStep(SessionFrame.STATE_DATUM_VAL, id, evaluateValue(ec)); + case SessionFrame.STATE_MULTIPLE_DATUM_VAL: + return new StackFrameStep(SessionFrame.STATE_MULTIPLE_DATUM_VAL, id, evaluateValue(ec)); case SessionFrame.STATE_COMMAND_ID: return new StackFrameStep(SessionFrame.STATE_COMMAND_ID, evaluateValue(ec), null); case SessionFrame.STATE_UNKNOWN: diff --git a/src/main/java/org/commcare/xml/StackFrameStepParser.java b/src/main/java/org/commcare/xml/StackFrameStepParser.java index dac8425428..2252c40a5e 100644 --- a/src/main/java/org/commcare/xml/StackFrameStepParser.java +++ b/src/main/java/org/commcare/xml/StackFrameStepParser.java @@ -26,10 +26,10 @@ class StackFrameStepParser extends ElementParser { public StackFrameStep parse() throws InvalidStructureException, IOException, XmlPullParserException { String operation = parser.getName(); String value = parser.getAttributeValue(null, "value"); + String datumId = parser.getAttributeValue(null, "id"); switch (operation) { case "datum": - String datumId = parser.getAttributeValue(null, "id"); return parseValue(SessionFrame.STATE_UNKNOWN, datumId); case "rewind": return parseValue(SessionFrame.STATE_REWIND, null); @@ -41,6 +41,8 @@ public StackFrameStep parse() throws InvalidStructureException, IOException, Xml return parseQuery(); case "jump": return parseJump(); + case "instance-datum": + return parseValue(SessionFrame.STATE_MULTIPLE_DATUM_VAL, datumId); default: throw new InvalidStructureException("<" + operation + "> is not a valid stack frame element!", this.parser); } From 289d2a2b9b29f940f6455c33d6c090cf30395584 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Tue, 15 Aug 2023 11:21:40 -0400 Subject: [PATCH 198/402] check for empty respones on required fields --- .../java/org/commcare/session/RemoteQuerySessionManager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java index e21b5644cd..1503ff49c1 100644 --- a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java +++ b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java @@ -286,6 +286,9 @@ private void validateUserAnswers() { if (!StringUtils.isEmpty(value) && queryPrompt.isInvalidInput(new EvaluationContext(ec, currentRef))) { errors.put(key, queryPrompt.getValidationMessage(ec)); } + if (StringUtils.isEmpty(value) && queryPrompt.isRequired(ec)) { + errors.put(key, queryPrompt.DEFAULT_REQUIRED_ERROR); + } } } From ccf270dbb2dcbf72fdc864dde960fbc3e9b5d42f Mon Sep 17 00:00:00 2001 From: robert-costello Date: Tue, 15 Aug 2023 11:22:14 -0400 Subject: [PATCH 199/402] update tests with expected errors --- .../backend/suite/model/test/CaseClaimModelTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java b/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java index 53f597b368..834ef96b8c 100644 --- a/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java +++ b/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java @@ -274,7 +274,7 @@ private ExternalDataInstance buildDistrictInstance() { public void testErrorsWithUserInput_noInput() throws Exception { testErrorsWithUserInput( ImmutableMap.of(), - ImmutableMap.of(), + ImmutableMap.of("age", "Sorry, this response is required!", "dob", "Sorry, this response is required!"), null ); } @@ -283,7 +283,7 @@ public void testErrorsWithUserInput_noInput() throws Exception { public void testErrorsWithUserInput_EmptyInput() throws Exception { testErrorsWithUserInput( ImmutableMap.of("age", "", "another_age", ""), - ImmutableMap.of(), + ImmutableMap.of("age", "Sorry, this response is required!"), null ); } From d9f5c79cedc9272a2115c3a13e1bb02251a296cc Mon Sep 17 00:00:00 2001 From: robert-costello Date: Tue, 15 Aug 2023 15:33:40 -0400 Subject: [PATCH 200/402] use custom required message if present --- .../org/commcare/session/RemoteQuerySessionManager.java | 6 +++++- .../backend/suite/model/test/CaseClaimModelTests.java | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java index 1503ff49c1..74e756292c 100644 --- a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java +++ b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java @@ -287,7 +287,11 @@ private void validateUserAnswers() { errors.put(key, queryPrompt.getValidationMessage(ec)); } if (StringUtils.isEmpty(value) && queryPrompt.isRequired(ec)) { - errors.put(key, queryPrompt.DEFAULT_REQUIRED_ERROR); + String message = queryPrompt.getRequiredMessage(ec); + if (message.replace('\u00A0',' ').isBlank()) { + message = queryPrompt.DEFAULT_REQUIRED_ERROR; + } + errors.put(key, message); } } } diff --git a/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java b/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java index 834ef96b8c..a8ebc3f023 100644 --- a/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java +++ b/src/test/java/org/commcare/backend/suite/model/test/CaseClaimModelTests.java @@ -274,7 +274,7 @@ private ExternalDataInstance buildDistrictInstance() { public void testErrorsWithUserInput_noInput() throws Exception { testErrorsWithUserInput( ImmutableMap.of(), - ImmutableMap.of("age", "Sorry, this response is required!", "dob", "Sorry, this response is required!"), + ImmutableMap.of("age", "One of age or DOB is required", "dob", "One of age or DOB is required"), null ); } @@ -283,7 +283,7 @@ public void testErrorsWithUserInput_noInput() throws Exception { public void testErrorsWithUserInput_EmptyInput() throws Exception { testErrorsWithUserInput( ImmutableMap.of("age", "", "another_age", ""), - ImmutableMap.of("age", "Sorry, this response is required!"), + ImmutableMap.of("age", "One of age or DOB is required"), null ); } From f60f7a0793e4c382e96aabd8fa1e0a8ec2a7de32 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Mon, 28 Aug 2023 13:29:47 -0400 Subject: [PATCH 201/402] consolidate functionality from recalculateRequired method --- .../session/RemoteQuerySessionManager.java | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java index 74e756292c..089609eb67 100644 --- a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java +++ b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java @@ -258,22 +258,11 @@ public void refreshItemSetChoices() { public void refreshInputDependentState() { refreshItemSetChoices(); validateUserAnswers(); - recalculateRequired(); } - private void recalculateRequired() { - requiredPrompts = new Hashtable<>(); - OrderedHashtable userInputDisplays = getNeededUserInputDisplays(); - EvaluationContext ec = getEvaluationContextWithUserInputInstance(); - for (Enumeration en = userInputDisplays.keys(); en.hasMoreElements(); ) { - String key = (String)en.nextElement(); - QueryPrompt queryPrompt = userInputDisplays.get(key); - boolean isRequired = queryPrompt.isRequired(ec); - requiredPrompts.put(key, isRequired); - } - } private void validateUserAnswers() { + requiredPrompts = new Hashtable<>(); errors = new Hashtable<>(); OrderedHashtable userInputDisplays = getNeededUserInputDisplays(); String instanceId = VirtualInstances.makeSearchInputInstanceID(getSearchInstanceReferenceId()); @@ -281,12 +270,14 @@ private void validateUserAnswers() { for (Enumeration en = userInputDisplays.keys(); en.hasMoreElements(); ) { String key = (String)en.nextElement(); QueryPrompt queryPrompt = userInputDisplays.get(key); + boolean isRequired = queryPrompt.isRequired(ec); String value = userAnswers.get(key); + requiredPrompts.put(key, isRequired); TreeReference currentRef = getReferenceToInstanceNode(instanceId, key); if (!StringUtils.isEmpty(value) && queryPrompt.isInvalidInput(new EvaluationContext(ec, currentRef))) { errors.put(key, queryPrompt.getValidationMessage(ec)); } - if (StringUtils.isEmpty(value) && queryPrompt.isRequired(ec)) { + if (StringUtils.isEmpty(value) && isRequired) { String message = queryPrompt.getRequiredMessage(ec); if (message.replace('\u00A0',' ').isBlank()) { message = queryPrompt.DEFAULT_REQUIRED_ERROR; From 75bf25ebb0751dc57ba1532802f3512a5c51b6db Mon Sep 17 00:00:00 2001 From: robert-costello Date: Thu, 31 Aug 2023 17:09:50 -0400 Subject: [PATCH 202/402] move line to better group logic --- .../java/org/commcare/session/RemoteQuerySessionManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java index 089609eb67..ddc1d88f75 100644 --- a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java +++ b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java @@ -271,8 +271,8 @@ private void validateUserAnswers() { String key = (String)en.nextElement(); QueryPrompt queryPrompt = userInputDisplays.get(key); boolean isRequired = queryPrompt.isRequired(ec); - String value = userAnswers.get(key); requiredPrompts.put(key, isRequired); + String value = userAnswers.get(key); TreeReference currentRef = getReferenceToInstanceNode(instanceId, key); if (!StringUtils.isEmpty(value) && queryPrompt.isInvalidInput(new EvaluationContext(ec, currentRef))) { errors.put(key, queryPrompt.getValidationMessage(ec)); From 9483e10515b58884dbefd1c0edb676cf3c7d16c6 Mon Sep 17 00:00:00 2001 From: robert-costello Date: Fri, 1 Sep 2023 12:10:10 -0400 Subject: [PATCH 203/402] simplify error message handling --- .../java/org/commcare/session/RemoteQuerySessionManager.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java index ddc1d88f75..7553bae0ea 100644 --- a/src/main/java/org/commcare/session/RemoteQuerySessionManager.java +++ b/src/main/java/org/commcare/session/RemoteQuerySessionManager.java @@ -279,9 +279,6 @@ private void validateUserAnswers() { } if (StringUtils.isEmpty(value) && isRequired) { String message = queryPrompt.getRequiredMessage(ec); - if (message.replace('\u00A0',' ').isBlank()) { - message = queryPrompt.DEFAULT_REQUIRED_ERROR; - } errors.put(key, message); } } From 52aeade34853ffbedd37558ee6db6706afe31637 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 1 Sep 2023 14:04:21 -0500 Subject: [PATCH 204/402] remove backward compatibility flag --- .../org/commcare/util/screen/QueryScreen.java | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/QueryScreen.java b/src/cli/java/org/commcare/util/screen/QueryScreen.java index 7eee1ee2e2..ca18fcc855 100644 --- a/src/cli/java/org/commcare/util/screen/QueryScreen.java +++ b/src/cli/java/org/commcare/util/screen/QueryScreen.java @@ -181,33 +181,10 @@ private String getInstanceKey(String instanceId, Map values) { return CryptUtil.sha256(builder.toString()); } - public void answerPrompts(Hashtable answers, boolean selectValuesByKeys) { + public void answerPrompts(Hashtable answers) { for (Enumeration en = userInputDisplays.keys(); en.hasMoreElements(); ) { String key = (String)en.nextElement(); - QueryPrompt queryPrompt = userInputDisplays.get(key); String answer = answers.get(key); - - // If select question, we should have got an index as the answer which should - // be converted to the corresponding value - if (queryPrompt.isSelect() && !StringUtils.isEmpty(answer)) { - remoteQuerySessionManager.populateItemSetChoices(queryPrompt); - if (!selectValuesByKeys) { - Vector selectChoices = queryPrompt.getItemsetBinding().getChoices(); - String[] indicesOfSelectedChoices = RemoteQuerySessionManager.extractMultipleChoices(answer); - ArrayList selectedChoices = new ArrayList<>(indicesOfSelectedChoices.length); - for (int i = 0; i < indicesOfSelectedChoices.length; i++) { - if (indicesOfSelectedChoices[i].isEmpty()) { - selectedChoices.add(""); - } else { - int choiceIndex = Integer.parseInt(indicesOfSelectedChoices[i]); - if (choiceIndex < selectChoices.size() && choiceIndex > -1) { - selectedChoices.add(selectChoices.get(choiceIndex).getValue()); - } - } - } - answer = String.join(RemoteQuerySessionManager.ANSWER_DELIMITER, selectedChoices); - } - } remoteQuerySessionManager.answerUserPrompt(key, answer); } remoteQuerySessionManager.refreshInputDependentState(); @@ -265,7 +242,7 @@ public boolean handleInputAndUpdateSession(CommCareSession session, String input userAnswers.put(queryPromptEntry.getKey(), answers[count]); count++; } - answerPrompts(userAnswers, true); + answerPrompts(userAnswers); URL url = getBaseUrl(); Multimap requestData = getQueryParams(false); InputStream response = sessionUtils.makeQueryRequest(url, requestData, domainedUsername, password); From bcdf40cfb2347a3034848e905880f9d5814bbd80 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 2 Aug 2023 12:51:50 -0500 Subject: [PATCH 205/402] remove stream() APIs as Android is not able to seamlessly support Java8 Apis on older devices --- .../org/commcare/suite/model/StackFrameStep.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/StackFrameStep.java b/src/main/java/org/commcare/suite/model/StackFrameStep.java index 96573031bb..4f8c43ac67 100644 --- a/src/main/java/org/commcare/suite/model/StackFrameStep.java +++ b/src/main/java/org/commcare/suite/model/StackFrameStep.java @@ -26,11 +26,11 @@ import java.io.DataOutputStream; import java.io.IOException; import java.util.Collection; +import java.util.HashMap; import java.util.Hashtable; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.stream.Collectors; /** * @author ctsims @@ -145,10 +145,13 @@ public void initDataInstanceSources(RemoteInstanceFetcher remoteInstanceFetcher) } public Map getInstances(InstanceInitializationFactory iif) { - return dataInstanceSources.values().stream().map((source) -> { - ExternalDataInstance instance = source.toInstance(); - return instance.initialize(iif, source.getInstanceId()); - }).collect(Collectors.toMap(DataInstance::getInstanceId, value -> value)); + HashMap instances = new HashMap<>(); + for (ExternalDataInstanceSource source : dataInstanceSources.values()) { + ExternalDataInstance instance = (ExternalDataInstance)source.toInstance() + .initialize(iif, source.getInstanceId()); + instances.put(instance.getInstanceId(), instance); + } + return instances; } /** From 5db37b9158f5c70a3e0514208a96a0403632d563 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Fri, 8 Sep 2023 23:58:31 +0200 Subject: [PATCH 206/402] Set locale to date format --- .../core/network/CommCareNetworkServiceGenerator.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/core/network/CommCareNetworkServiceGenerator.java b/src/main/java/org/commcare/core/network/CommCareNetworkServiceGenerator.java index d50a9c5558..17e0ae88ef 100644 --- a/src/main/java/org/commcare/core/network/CommCareNetworkServiceGenerator.java +++ b/src/main/java/org/commcare/core/network/CommCareNetworkServiceGenerator.java @@ -12,9 +12,11 @@ import org.javarosa.core.services.Logger; import java.io.IOException; +import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -73,8 +75,10 @@ public class CommCareNetworkServiceGenerator { ICommCarePreferenceManager commCarePreferenceManager = CommCarePreferenceManagerFactory.getCommCarePreferenceManager(); if (commCarePreferenceManager != null) { String serverDate = response.header("date"); + try { - long serverTimeInMillis = new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss zzz").parse(serverDate).getTime(); + long serverTimeInMillis = new SimpleDateFormat( + "EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH).parse(serverDate).getTime(); long now = new Date().getTime(); long currentDrift = (now - serverTimeInMillis) / HOUR_IN_MS; commCarePreferenceManager.putLong(CURRENT_DRIFT, currentDrift); @@ -90,6 +94,8 @@ public class CommCareNetworkServiceGenerator { return response; }; + + private static AuthenticationInterceptor authenticationInterceptor = new AuthenticationInterceptor(); private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder() From bcac8ee41fa58cfd97decc84906c2600fc8d281e Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 8 Sep 2023 12:24:46 -0500 Subject: [PATCH 207/402] Parse stack query data as QueryData model --- .../java/org/commcare/xml/StackFrameStepParser.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/commcare/xml/StackFrameStepParser.java b/src/main/java/org/commcare/xml/StackFrameStepParser.java index 2252c40a5e..e310aca99e 100644 --- a/src/main/java/org/commcare/xml/StackFrameStepParser.java +++ b/src/main/java/org/commcare/xml/StackFrameStepParser.java @@ -1,6 +1,7 @@ package org.commcare.xml; import org.commcare.session.SessionFrame; +import org.commcare.suite.model.QueryData; import org.commcare.suite.model.StackFrameStep; import org.javarosa.xml.ElementParser; import org.javarosa.xml.util.InvalidStructureException; @@ -62,14 +63,8 @@ private StackFrameStep parseQuery() throws InvalidStructureException, IOExceptio while (nextTagInBlock("query")) { String tagName = parser.getName(); if ("data".equals(tagName)) { - String key = parser.getAttributeValue(null, "key"); - String ref = parser.getAttributeValue(null, "ref"); - try { - step.addExtra(key, XPathParseTool.parseXPath(ref)); - } catch (XPathSyntaxException e) { - String errorMessage = "'ref' value is not a valid xpath expression: " + ref; - throw new InvalidStructureException(errorMessage, this.parser); - } + QueryData queryData = new QueryDataParser(parser).parse(); + step.addExtra(queryData.getKey(), queryData); } } return step; From 0761ac4e561508cdc08dea2dd7cd70a59491efa1 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 8 Sep 2023 12:25:54 -0500 Subject: [PATCH 208/402] handle queryData for step extras --- src/main/java/org/commcare/suite/model/StackFrameStep.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/commcare/suite/model/StackFrameStep.java b/src/main/java/org/commcare/suite/model/StackFrameStep.java index 1a38ec75e1..ed7bdccdab 100644 --- a/src/main/java/org/commcare/suite/model/StackFrameStep.java +++ b/src/main/java/org/commcare/suite/model/StackFrameStep.java @@ -219,8 +219,11 @@ public StackFrameStep defineStep(EvaluationContext ec, SessionDatum neededDatum) case SessionFrame.STATE_SMART_LINK: StackFrameStep defined = new StackFrameStep(elementType, id, evaluateValue(ec)); extras.forEach((key, value) -> { - XPathExpression expr = (XPathExpression) value; - defined.addExtra(key, FunctionUtils.toString(expr.eval(ec))); + if (value instanceof QueryData) { + defined.addExtra(key, ((QueryData)value).getValues(ec)); + } else { + throw new RuntimeException("Invalid data type for step extra " + key); + } }); return defined; default: From fbd686bc958a1239418a91eeac58acae10bbfcf8 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 8 Sep 2023 12:40:58 -0500 Subject: [PATCH 209/402] test fix: query extras are QueryData and hence a list of string --- .../suite/model/test/StackFrameStepTests.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/test/java/org/commcare/backend/suite/model/test/StackFrameStepTests.java b/src/test/java/org/commcare/backend/suite/model/test/StackFrameStepTests.java index 8e172c0c57..ac33963b01 100644 --- a/src/test/java/org/commcare/backend/suite/model/test/StackFrameStepTests.java +++ b/src/test/java/org/commcare/backend/suite/model/test/StackFrameStepTests.java @@ -1,5 +1,11 @@ package org.commcare.backend.suite.model.test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import org.cli.MockSessionUtils; @@ -20,11 +26,6 @@ import java.io.InputStream; import java.util.Vector; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - /** * Created by amstone326 on 8/7/15. */ @@ -180,11 +181,11 @@ public void stackWithQueries() throws Exception { StackFrameStep queryFrame = steps.get(2); assertEquals(SessionFrame.STATE_QUERY_REQUEST, queryFrame.getElementType()); - ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); - builder.put("case_type", "patient"); - builder.put("x_commcare_data_registry", "test"); - builder.put("case_id", "case_one"); - builder.put("case_id", "dupe1"); + ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); + builder.put("case_type", ImmutableList.of("patient")); + builder.put("x_commcare_data_registry", ImmutableList.of("test")); + builder.put("case_id", ImmutableList.of("case_one")); + builder.put("case_id", ImmutableList.of("dupe1")); assertEquals(builder.build(), queryFrame.getExtras()); } } From 7055c5934d8473ce73b9b41ef2c6b2e8848b0439 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Fri, 22 Sep 2023 16:45:51 -0500 Subject: [PATCH 210/402] Clickable icon UI --- src/main/java/org/commcare/suite/model/Style.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/commcare/suite/model/Style.java b/src/main/java/org/commcare/suite/model/Style.java index 32fa390942..dea02e4a29 100644 --- a/src/main/java/org/commcare/suite/model/Style.java +++ b/src/main/java/org/commcare/suite/model/Style.java @@ -41,6 +41,7 @@ enum DisplayFormat { Graph, Phone, Markdown, + ClickableIcon, } public DisplayFormat getDisplayFormat() { @@ -94,6 +95,9 @@ private void setDisplayFormatFromString(String displayFormat){ case "markdown": setDisplayFormat(DisplayFormat.Markdown); break; + case "clickable-icon": + setDisplayFormat(DisplayFormat.ClickableIcon); + break; } } From 044fc318d4d98d9a60b686d00e9542dfa0f693e8 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Mon, 9 Oct 2023 11:26:44 -0500 Subject: [PATCH 211/402] prints error message from merge conflicting arising from cherry-picking --- scripts/duplicate_pr.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 68a31f2ef8..573466485e 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -68,14 +68,17 @@ def get_new_commits(base_branch: str, curr_branch:str, base_commit:str = None): def cherry_pick_new_commits(commits:list[str], branch:str): git = get_git() git.checkout(branch) - for commits in reversed(commits): + for commit in reversed(commits): try: empty_commit_message = "The previous cherry-pick is now empty" - git("cherry-pick", commits) + failed_cherry_pick = "error: could not apply" + git("cherry-pick", commit) except sh.ErrorReturnCode_1 as e: if empty_commit_message in e.stderr.decode(): git("cherry-pick", "--skip") - + if failed_cherry_pick in e.stderr.decode(): + print(red(f'''Failed to cherry-pick {commit} most likely from a merge conflict with branch {branch}. The PR will need to either be manually duplicated or rebased to resolve the merge conflict.''')) + raise def git_push_pr(branch:str): git = get_git() From bc709b4d526e5eecda276b90d8f6096f8ac0d082 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Mon, 9 Oct 2023 14:14:29 -0500 Subject: [PATCH 212/402] bug: when the PR is merged, the last commit is the merge into the target branch. This merge commit may have multiple parents and there is no way of knowing which parent we should use to search for commits of interest. This allows the user to define the last commit of interest that is not the merge. This will be used to pull the range of commits of interest. --- .github/workflows/duplicate_pr.yml | 13 +++++++++++-- scripts/duplicate_pr.py | 10 ++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/duplicate_pr.yml b/.github/workflows/duplicate_pr.yml index 47a7c022af..f7a69a48d3 100644 --- a/.github/workflows/duplicate_pr.yml +++ b/.github/workflows/duplicate_pr.yml @@ -18,7 +18,15 @@ jobs: uses: actions/github-script@0.9.0 with: script: | - const sha = context.payload.comment.body.split(" ").pop() + const sha = context.payload.comment.body.split(" ").reverse()[1]; + core.setOutput('sha', sha) + - name: Get PR End Commit SHA + if: ${{ github.event.issue.pull_request.merged_at }} + id: end-sha + uses: actions/github-script@0.9.0 + with: + script: | + const sha = context.payload.comment.body.split(" ").reverse()[0]; core.setOutput('sha', sha) # https://github.com/marketplace/actions/pull-request-comment-branch - name: Pull Request Comment Branch @@ -41,9 +49,10 @@ jobs: if: ${{ github.event.issue.pull_request.merged_at }} run: | echo Intial SHA is "${{ steps.initial-sha.outputs.sha }}" - python scripts/duplicate_pr.py ${{ steps.comment-branch.outputs.head_ref }} ${{ steps.comment-branch.outputs.base_ref }} --initial_sha ${initial_sha} + python scripts/duplicate_pr.py ${{ steps.comment-branch.outputs.head_ref }} ${{ steps.comment-branch.outputs.base_ref }} --initial_sha ${initial_sha} --end_sha ${end_sha} env: initial_sha: ${{ steps.initial-sha.outputs.sha }} + end_sha: ${{ steps.end-sha.outputs.sha }} - name: Run duplicate_pr.py script with PR not merged if: ${{ github.event.issue.pull_request.merged_at == null }} run: python scripts/duplicate_pr.py ${{ steps.comment-branch.outputs.head_ref }} ${{ steps.comment-branch.outputs.base_ref }} diff --git a/scripts/duplicate_pr.py b/scripts/duplicate_pr.py index 68a31f2ef8..d155e50e7a 100644 --- a/scripts/duplicate_pr.py +++ b/scripts/duplicate_pr.py @@ -50,7 +50,7 @@ def git_fetch_branch(branch_name:str): exit(1) -def get_new_commits(base_branch: str, curr_branch:str, base_commit:str = None): +def get_new_commits(base_branch: str, curr_branch:str, base_commit:str = None, end_commit:str = None): git = get_git() if base_branch != BranchName.MASTER.value: git_fetch_branch(base_branch) @@ -58,9 +58,10 @@ def get_new_commits(base_branch: str, curr_branch:str, base_commit:str = None): base_commit = git.show("{}^1".format(base_commit)).split()[1] else: base_commit = merge_base_commit(base_branch, curr_branch) - recent_commit = latest_commit(curr_branch) + if not end_commit: + end_commit = latest_commit(curr_branch) - commits_range = "{}..{}".format(base_commit, recent_commit) + commits_range = "{}..{}".format(base_commit, end_commit) interested_commits = git("rev-list", "--no-merges", "--first-parent", commits_range).split() return interested_commits @@ -116,6 +117,7 @@ def main(): parser.add_argument('orig_target_branch', type=str, help="Name of branch the original PR merged into", choices = [key.value for key in BranchName]) parser.add_argument('-i','--initial_sha', type=str, help="SHA of first commit in PR to be duplicated") + parser.add_argument('-e','--end_sha', type=str, help="SHA of last commit in PR to be duplicated") args = parser.parse_args() new_source_branch = "copy_of_" + args.orig_source_branch @@ -128,7 +130,7 @@ def main(): git_fetch_branch(args.orig_source_branch) print("Getting new commits from {}".format(args.orig_source_branch)) - new_commits = get_new_commits(args.orig_target_branch, args.orig_source_branch, args.initial_sha) + new_commits = get_new_commits(args.orig_target_branch, args.orig_source_branch, args.initial_sha, args.end_sha) print("Cherry-picking commits from {} to {}".format(args.orig_source_branch, new_source_branch)) cherry_pick_new_commits(new_commits, new_source_branch) From 29ecf66fabe3fa31998fbe65536a1c6eb9d613c4 Mon Sep 17 00:00:00 2001 From: Jonathan Tang Date: Mon, 9 Oct 2023 14:30:55 -0500 Subject: [PATCH 213/402] adds instructions on ending-commit-id parameter for duplicating PR --- .github/contributing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/contributing.md b/.github/contributing.md index 92c9466d8e..74181ade73 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -8,7 +8,7 @@ We try to keep both these branches in sync with each other the best we can. To d 1. If you are working on a Formplayer change, you will want to start by checking out `your_feature_branch` from `formplayer` as the base branch. Make changes on `your_feature_branch` and create your original PR against `formplayer` branch. -2. Now you will need to duplicate this PR by making another PR against `master`. Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. This should result in a Github Actions workflow duplicating your PR against `master`. Go to the duplicate PR, close and re-open it to run the Github checks against it. +2. Now you will need to duplicate this PR by making another PR against `master`. Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. The `ending-commit-id` should be the last non-merge commit in the PR. This should result in a Github Actions workflow duplicating your PR against `master`. Go to the duplicate PR, close and re-open it to run the Github checks against it. 3. In order for us to test that your PR against `master` doesn't break anything on CommCare Android, you need to run android side tests with your PR. To do this - @@ -23,7 +23,7 @@ To do this - 1. If you are working on a CommCare Android change, you will want to start by checking out `your_feature_branch` from `master` as the base branch. Make changes on `your_feature_branch` and create your original PR against `master` branch. -2. Now you will need to duplicate this PR by making another PR against `formplayer`. Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. This should result in a Github Actions workflow duplicating your PR against `formplayer`. Go to the duplicate PR, close and re-open it to run the Github checks against it. +2. Now you will need to duplicate this PR by making another PR against `formplayer`. Make sure the branch for this PR is not deleted. Then create the comment `duplicate this PR`. If the PR has already been merged, comment `duplicate this PR `. The `ending-commit-id` should be the last non-merge commit in the PR. This should result in a Github Actions workflow duplicating your PR against `formplayer`. Go to the duplicate PR, close and re-open it to run the Github checks against it. 3. In order for us to test that your PR against `formplayer` doesn't break anything on Formplayer, we need to run formplayer side tests with your PR. To do this - From 83adaf85f09fe53169ce001860796aa0c34566e2 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 16 Jun 2023 14:24:05 -0400 Subject: [PATCH 214/402] Adds alignment to detail field style --- src/main/java/org/commcare/suite/model/Style.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/org/commcare/suite/model/Style.java b/src/main/java/org/commcare/suite/model/Style.java index dea02e4a29..a53eb67c43 100644 --- a/src/main/java/org/commcare/suite/model/Style.java +++ b/src/main/java/org/commcare/suite/model/Style.java @@ -8,6 +8,8 @@ public class Style { private DisplayFormat displayFormats; private int fontSize; private int widthHint; + private String horizontalAlign; + private String verticalAlign; public Style(){} @@ -30,6 +32,9 @@ public Style(DetailField detail){ setWidthHint(-1); } setDisplayFormatFromString(detail.getTemplateForm()); + + verticalAlign = detail.getVerticalAlign(); + horizontalAlign = detail.getHorizontalAlign(); } enum DisplayFormat { @@ -105,4 +110,12 @@ private void setDisplayFormatFromString(String displayFormat){ public String toString(){ return "Style: [displayFormat=" + displayFormats + ", fontSize=" + fontSize + "]"; } + + public String getHorizontalAlign() { + return horizontalAlign; + } + + public String getVerticalAlign() { + return verticalAlign; + } } From f5aed82ea5278ff3f6d53f3217f4d3857fb3eb1a Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 16 Jun 2023 15:13:41 -0400 Subject: [PATCH 215/402] Adds missing fields to serialization methods --- src/main/java/org/commcare/suite/model/DetailField.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/commcare/suite/model/DetailField.java b/src/main/java/org/commcare/suite/model/DetailField.java index 18be0e8826..33c09c20ec 100644 --- a/src/main/java/org/commcare/suite/model/DetailField.java +++ b/src/main/java/org/commcare/suite/model/DetailField.java @@ -198,6 +198,9 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOExcep gridHeight = ExtUtil.readInt(in); fontSize = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); showBlanksLastInSort = ExtUtil.readBool(in); + horizontalAlign = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); + verticalAlign = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); + cssID = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); } @Override @@ -224,6 +227,9 @@ public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.writeNumeric(out, gridHeight); ExtUtil.writeString(out, ExtUtil.emptyIfNull(fontSize)); ExtUtil.writeBool(out, showBlanksLastInSort); + ExtUtil.writeString(out, ExtUtil.emptyIfNull(horizontalAlign)); + ExtUtil.writeString(out, ExtUtil.emptyIfNull(verticalAlign)); + ExtUtil.writeString(out, ExtUtil.emptyIfNull(cssID)); } public int getGridX() { From de1002360689982d95f9be5bff2d6d1cbf305cc1 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Wed, 4 Oct 2023 11:19:31 -0500 Subject: [PATCH 216/402] Adds endpoint_action to detail field --- .../org/commcare/suite/model/DetailField.java | 16 +++++++ .../commcare/suite/model/EndpointAction.java | 45 +++++++++++++++++++ .../org/commcare/xml/DetailFieldParser.java | 18 +++++++- .../suite/model/test/AppStructureTests.java | 11 +++++ src/test/resources/app_structure/suite.xml | 1 + 5 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/commcare/suite/model/EndpointAction.java diff --git a/src/main/java/org/commcare/suite/model/DetailField.java b/src/main/java/org/commcare/suite/model/DetailField.java index 33c09c20ec..ea812612da 100644 --- a/src/main/java/org/commcare/suite/model/DetailField.java +++ b/src/main/java/org/commcare/suite/model/DetailField.java @@ -17,6 +17,8 @@ import java.io.DataOutputStream; import java.io.IOException; +import javax.annotation.Nullable; + /** * Detail Fields represent the elements of a suite's detail * definitions. The model contains the relevent text templates as well @@ -44,6 +46,9 @@ public class DetailField implements Externalizable { private String templateWidthHint = null; private String printIdentifier; + @Nullable + private EndpointAction endpointAction; + /** * Optional hint which provides a hint for whether rich media should be * displayed based on returning a URI. May be either 'image' or @@ -201,6 +206,7 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOExcep horizontalAlign = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); verticalAlign = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); cssID = ExtUtil.nullIfEmpty(ExtUtil.readString(in)); + endpointAction = (EndpointAction)ExtUtil.read(in, new ExtWrapNullable(EndpointAction.class), pf); } @Override @@ -230,6 +236,7 @@ public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.writeString(out, ExtUtil.emptyIfNull(horizontalAlign)); ExtUtil.writeString(out, ExtUtil.emptyIfNull(verticalAlign)); ExtUtil.writeString(out, ExtUtil.emptyIfNull(cssID)); + ExtUtil.write(out, new ExtWrapNullable(endpointAction)); } public int getGridX() { @@ -256,6 +263,11 @@ public String getVerticalAlign() { return verticalAlign; } + @Nullable + public EndpointAction getEndpointAction() { + return endpointAction; + } + public String getFontSize() { return fontSize; } @@ -382,5 +394,9 @@ public void setFontSize(String fontSize) { public void setCssID(String id) { field.cssID = id; } + + public void setEndpointAction(EndpointAction endpointAction) { + field.endpointAction = endpointAction; + } } } diff --git a/src/main/java/org/commcare/suite/model/EndpointAction.java b/src/main/java/org/commcare/suite/model/EndpointAction.java new file mode 100644 index 0000000000..e9e7eb37b2 --- /dev/null +++ b/src/main/java/org/commcare/suite/model/EndpointAction.java @@ -0,0 +1,45 @@ +package org.commcare.suite.model; + +import org.javarosa.core.util.externalizable.DeserializationException; +import org.javarosa.core.util.externalizable.ExtUtil; +import org.javarosa.core.util.externalizable.Externalizable; +import org.javarosa.core.util.externalizable.PrototypeFactory; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class EndpointAction implements Externalizable { + + private String endpointId; + private boolean isBackground; + + public EndpointAction() { + } + + public EndpointAction(String endpointId, boolean isBackground) { + this.endpointId = endpointId; + this.isBackground = isBackground; + } + + @Override + public void readExternal(DataInputStream in, PrototypeFactory pf) + throws IOException, DeserializationException { + endpointId = ExtUtil.readString(in); + isBackground = ExtUtil.readBool(in); + } + + @Override + public void writeExternal(DataOutputStream out) throws IOException { + ExtUtil.writeString(out, endpointId); + ExtUtil.writeBool(out, isBackground); + } + + public String getEndpointId() { + return endpointId; + } + + public boolean isBackground() { + return isBackground; + } +} diff --git a/src/main/java/org/commcare/xml/DetailFieldParser.java b/src/main/java/org/commcare/xml/DetailFieldParser.java index caf439a057..7cb34e5144 100644 --- a/src/main/java/org/commcare/xml/DetailFieldParser.java +++ b/src/main/java/org/commcare/xml/DetailFieldParser.java @@ -2,6 +2,7 @@ import org.commcare.suite.model.DetailField; import org.commcare.suite.model.DetailTemplate; +import org.commcare.suite.model.EndpointAction; import org.commcare.suite.model.Text; import org.javarosa.core.model.Constants; import org.javarosa.xml.util.InvalidStructureException; @@ -73,9 +74,9 @@ public DetailField parse() throws InvalidStructureException, IOException, XmlPul } else { throw new InvalidStructureException("detail with no - + From 022657acee44c784678dfde995914a195e9e6965 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 6 Jan 2025 19:30:10 +0530 Subject: [PATCH 315/402] Adds a method to check if we should trigger any optimizations --- src/main/java/org/commcare/suite/model/Detail.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/org/commcare/suite/model/Detail.java b/src/main/java/org/commcare/suite/model/Detail.java index e726abb41c..84b95bd181 100755 --- a/src/main/java/org/commcare/suite/model/Detail.java +++ b/src/main/java/org/commcare/suite/model/Detail.java @@ -515,6 +515,18 @@ public boolean hasSortField() { return false; } + // Returns true if we should trigger any optimizations for this detail + public boolean shouldOptimize() { + if (cacheEnabled || lazyLoading) { + for (DetailField field : fields) { + if (field.isOptimize()) { + return true; + } + } + } + return false; + } + private abstract class Map { private final E a; From 6691df6b5e5baba47d440ef1439b23fd0b5a50ab Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 7 Jan 2025 17:04:18 +0530 Subject: [PATCH 316/402] Implements cache get for getField --- .../commcare/cases/entity/AsyncEntity.java | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncEntity.java b/src/main/java/org/commcare/cases/entity/AsyncEntity.java index d51fe837c1..4b1d3011b7 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncEntity.java +++ b/src/main/java/org/commcare/cases/entity/AsyncEntity.java @@ -107,18 +107,43 @@ private void loadVariableContext() { @Override public Object getField(int i) { synchronized (mAsyncLock) { - loadVariableContext(); - if (data[i] == null) { - try { - data[i] = fields[i].getTemplate().evaluate(context); - } catch (XPathException xpe) { - Logger.exception("Error while evaluating field for case list ", xpe); - xpe.printStackTrace(); - data[i] = ""; + if (data[i] != null) { + return data[i]; + } + } + try (Closeable ignored = mEntityStorageCache != null ? mEntityStorageCache.lockCache() : null) { + synchronized (mAsyncLock) { + if (data[i] == null) { + String cacheKey = null; + if (mEntityStorageCache != null && mCacheIndex != null) { + // todo need separation between cache keys of getField and getSortField + cacheKey = mEntityStorageCache.getCacheKey(mDetailId, String.valueOf(i)); + // Return from the cache if we have a value + String value = mEntityStorageCache.retrieveCacheValue(mCacheIndex, cacheKey); + if (value != null) { + data[i] = value; + return data[i]; + } + } + // Otherwise evaluate, cache and return the value + loadVariableContext(); + try { + data[i] = fields[i].getTemplate().evaluate(context); + if (mEntityStorageCache != null && mCacheIndex != null) { + mEntityStorageCache.cache(mCacheIndex, cacheKey, String.valueOf(data[i])); + } + } catch (XPathException xpe) { + Logger.exception("Error while evaluating field for case list ", xpe); + xpe.printStackTrace(); + data[i] = ""; + } } + return data[i]; } - return data[i]; + } catch (IOException e) { + Logger.exception("Error while getting field", e); } + return null; } @Override @@ -141,7 +166,6 @@ public String getSortField(int i) { //get our second lock. synchronized (mAsyncLock) { if (sortData[i] == null) { - // sort data not in search field cache; load and store it Text sortText = fields[i].getSort(); if (sortText == null) { return null; @@ -157,6 +181,7 @@ public String getSortField(int i) { return sortData[i]; } + // sort data not in search field cache; load and store it loadVariableContext(); try { sortText = fields[i].getSort(); From 5e277528e85f1ac8b8342611da143a76fc062448 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 9 Jan 2025 16:15:08 +0530 Subject: [PATCH 317/402] Modifications for caching normal field value along with sort value for fields marked as optimize --- .../commcare/cases/entity/AsyncEntity.java | 112 ++++++++++++------ .../cases/entity/AsyncNodeEntityFactory.java | 35 +++++- .../cases/entity/EntityStorageCache.java | 9 +- 3 files changed, 114 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncEntity.java b/src/main/java/org/commcare/cases/entity/AsyncEntity.java index 4b1d3011b7..05b7da55cf 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncEntity.java +++ b/src/main/java/org/commcare/cases/entity/AsyncEntity.java @@ -1,6 +1,9 @@ package org.commcare.cases.entity; +import static org.commcare.cases.entity.EntityStorageCache.ValueType.TYPE_NORMAL_FIELD; +import static org.commcare.cases.entity.EntityStorageCache.ValueType.TYPE_SORT_FIELD; + import org.commcare.cases.util.StringUtils; import org.commcare.suite.model.DetailField; import org.commcare.suite.model.DetailGroup; @@ -46,6 +49,7 @@ public class AsyncEntity extends Entity { private final EvaluationContext context; private final Hashtable mVariableDeclarations; private final DetailGroup mDetailGroup; + private final boolean cacheEnabled; private boolean mVariableContextLoaded = false; private final String mCacheIndex; @@ -70,7 +74,7 @@ public class AsyncEntity extends Entity { public AsyncEntity(DetailField[] fields, EvaluationContext ec, TreeReference t, Hashtable variables, @Nullable EntityStorageCache cache, String cacheIndex, String detailId, - String extraKey, DetailGroup detailGroup) { + String extraKey, DetailGroup detailGroup, boolean cacheEnabled) { super(t, extraKey); this.fields = fields; this.data = new Object[fields.length]; @@ -88,6 +92,7 @@ public AsyncEntity(DetailField[] fields, EvaluationContext ec, this.mDetailId = detailId; this.mDetailGroup = detailGroup; + this.cacheEnabled = cacheEnabled; } private void loadVariableContext() { @@ -110,14 +115,17 @@ public Object getField(int i) { if (data[i] != null) { return data[i]; } + if (!fields[i].isOptimize()) { + data[i] = evaluateField(i); + return data[i]; + } } try (Closeable ignored = mEntityStorageCache != null ? mEntityStorageCache.lockCache() : null) { synchronized (mAsyncLock) { if (data[i] == null) { String cacheKey = null; if (mEntityStorageCache != null && mCacheIndex != null) { - // todo need separation between cache keys of getField and getSortField - cacheKey = mEntityStorageCache.getCacheKey(mDetailId, String.valueOf(i)); + cacheKey = mEntityStorageCache.getCacheKey(mDetailId, String.valueOf(i), TYPE_NORMAL_FIELD); // Return from the cache if we have a value String value = mEntityStorageCache.retrieveCacheValue(mCacheIndex, cacheKey); if (value != null) { @@ -126,16 +134,9 @@ public Object getField(int i) { } } // Otherwise evaluate, cache and return the value - loadVariableContext(); - try { - data[i] = fields[i].getTemplate().evaluate(context); - if (mEntityStorageCache != null && mCacheIndex != null) { - mEntityStorageCache.cache(mCacheIndex, cacheKey, String.valueOf(data[i])); - } - } catch (XPathException xpe) { - Logger.exception("Error while evaluating field for case list ", xpe); - xpe.printStackTrace(); - data[i] = ""; + data[i] = evaluateField(i); + if (mEntityStorageCache != null && mCacheIndex != null) { + mEntityStorageCache.cache(mCacheIndex, cacheKey, String.valueOf(data[i])); } } return data[i]; @@ -146,6 +147,18 @@ public Object getField(int i) { return null; } + private Object evaluateField(int i) { + loadVariableContext(); + try { + data[i] = fields[i].getTemplate().evaluate(context); + } catch (XPathException xpe) { + Logger.exception("Error while evaluating field for case list ", xpe); + xpe.printStackTrace(); + data[i] = ""; + } + return data[i]; + } + @Override public String getNormalizedField(int i) { String normalized = this.getSortField(i); @@ -161,6 +174,12 @@ public String getSortField(int i) { if (sortData[i] != null) { return sortData[i]; } + + // eval and return if field is not marked as optimize + if (cacheEnabled && !fields[i].isOptimize()) { + evaluateSortData(i); + return sortData[i]; + } } try (Closeable ignored = mEntityStorageCache != null ? mEntityStorageCache.lockCache() : null) { //get our second lock. @@ -170,9 +189,15 @@ public String getSortField(int i) { if (sortText == null) { return null; } - String cacheKey = null; + String cacheKey; if (mEntityStorageCache != null) { - cacheKey = mEntityStorageCache.getCacheKey(mDetailId, String.valueOf(i)); + if (cacheEnabled) { + cacheKey = mEntityStorageCache.getCacheKey(mDetailId, String.valueOf(i), + TYPE_SORT_FIELD); + } else { + // old cache and index + cacheKey = i + "_" + TYPE_SORT_FIELD; + } if (mCacheIndex != null) { //Check the cache! String value = mEntityStorageCache.retrieveCacheValue(mCacheIndex, cacheKey); @@ -180,24 +205,9 @@ public String getSortField(int i) { this.setSortData(i, value); return sortData[i]; } - // sort data not in search field cache; load and store it - loadVariableContext(); - try { - sortText = fields[i].getSort(); - if (sortText == null) { - this.setSortData(i, getFieldString(i)); - } else { - this.setSortData(i, StringUtils.normalize(sortText.evaluate(context))); - } - if (mEntityStorageCache != null) { - mEntityStorageCache.cache(mCacheIndex, cacheKey, sortData[i]); - } - } catch (XPathException xpe) { - Logger.exception("Error while evaluating sort field", xpe); - xpe.printStackTrace(); - sortData[i] = ""; - } + evaluateSortData(i); + mEntityStorageCache.cache(mCacheIndex, cacheKey, sortData[i]); } } } @@ -209,6 +219,22 @@ public String getSortField(int i) { return null; } + private void evaluateSortData(int i) { + loadVariableContext(); + try { + Text sortText = fields[i].getSort(); + if (sortText == null) { + this.setSortData(i, getFieldString(i)); + } else { + this.setSortData(i, StringUtils.normalize(sortText.evaluate(context))); + } + } catch (XPathException xpe) { + Logger.exception("Error while evaluating sort field", xpe); + xpe.printStackTrace(); + sortData[i] = ""; + } + } + @Override public int getNumFields() { return fields.length; @@ -259,13 +285,29 @@ private void setSortData(int i, String val) { } } + private void setFieldData(int i, String val) { + synchronized (mAsyncLock) { + data[i] = val; + } + } + public void setSortData(String cacheKey, String val) { if (mEntityStorageCache == null) { throw new IllegalStateException("No entity cache defined"); } - int sortIndex = mEntityStorageCache.getSortFieldIdFromCacheKey(mDetailId, cacheKey); - if (sortIndex != -1) { - setSortData(sortIndex, val); + int fieldIndex = mEntityStorageCache.getFieldIdFromCacheKey(mDetailId, cacheKey); + if (fieldIndex != -1) { + setSortData(fieldIndex, val); + } + } + + public void setFieldData(String cacheKey, String val) { + if (mEntityStorageCache == null) { + throw new IllegalStateException("No entity cache defined"); + } + int fieldIndex = mEntityStorageCache.getFieldIdFromCacheKey(mDetailId, cacheKey); + if (fieldIndex != -1) { + setFieldData(fieldIndex, val); } } diff --git a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java index d876389f4f..62a3608b6c 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java @@ -57,7 +57,7 @@ public Entity getEntity(TreeReference data) { String entityKey = loadCalloutDataMapKey(nodeContext); AsyncEntity entity = new AsyncEntity(detail.getFields(), nodeContext, data, mVariableDeclarations, - mEntityCache, mCacheIndex, detail.getId(), entityKey, detail.getGroup()); + mEntityCache, mCacheIndex, detail.getId(), entityKey, detail.getGroup(), detail.isCacheEnabled()); if (mCacheIndex != null) { mEntitySet.put(mCacheIndex, entity); @@ -106,7 +106,7 @@ protected void prepareEntitiesInternal(List> entities) { // if blocking mode load cache on the same thread and set any data thats not cached if (isBlockingAsyncMode) { primeCache(); - setUnCachedData(entities); + setUnCachedDataOld(entities); } else { // otherwise we want to show the entity list asap and hence want to offload the loading cache part to a separate // thread while caching any uncached data later on UI thread during Adapter's getView @@ -121,8 +121,13 @@ protected void prepareEntitiesInternal(List> entities) { @Override public void cacheEntities(List> entities) { - primeCache(); - setUnCachedData(entities); + if (detail.isCacheEnabled()) { + primeCache(); + setUnCachedData(entities); + } else { + primeCache(); + setUnCachedDataOld(entities); + } } protected void setUnCachedData(List> entities) { @@ -130,7 +135,27 @@ protected void setUnCachedData(List> entities) { if (isCancelled) return; AsyncEntity e = (AsyncEntity)entities.get(i); for (int col = 0; col < e.getNumFields(); ++col) { - e.getSortField(col); + if (detail.getFields()[col].isOptimize()) { + e.getField(col); + e.getSortField(col); + } + } + if (progressListener != null) { + progressListener.publishEntityLoadingProgress( + EntityLoadingProgressListener.EntityLoadingProgressPhase.PHASE_UNCACHED_CALCULATION, i, + entities.size()); + } + } + } + + // Old cache and index pathway where we only cache sort fields + @Deprecated + protected void setUnCachedDataOld(List> entities) { + for (int i = 0; i < entities.size(); i++) { + if (isCancelled) return; + AsyncEntity e = (AsyncEntity)entities.get(i); + for (int col = 0; col < e.getNumFields(); ++col) { + e.getSortField(col); } if (progressListener != null) { progressListener.publishEntityLoadingProgress( diff --git a/src/main/java/org/commcare/cases/entity/EntityStorageCache.java b/src/main/java/org/commcare/cases/entity/EntityStorageCache.java index 589d3c0895..69ea8163ac 100644 --- a/src/main/java/org/commcare/cases/entity/EntityStorageCache.java +++ b/src/main/java/org/commcare/cases/entity/EntityStorageCache.java @@ -9,15 +9,20 @@ * Interface for evaluated entity fields cache */ public interface EntityStorageCache { + enum ValueType { + TYPE_NORMAL_FIELD, + TYPE_SORT_FIELD + } + Closeable lockCache(); - String getCacheKey(String detailId, String detailFieldIndex); + String getCacheKey(String detailId, String detailFieldIndex, ValueType valueType); String retrieveCacheValue(String cacheIndex, String cacheKey); void cache(String cacheIndex, String cacheKey, String data); - int getSortFieldIdFromCacheKey(String detailId, String cacheKey); + int getFieldIdFromCacheKey(String detailId, String cacheKey); void primeCache(Hashtable entitySet, String[][] cachePrimeKeys, Detail detail); } From 092008bdbded3dfb9ffa4ea6915e1a8024a28a99 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 10 Jan 2025 10:57:25 +0530 Subject: [PATCH 318/402] Derive constructor params directly from detail --- .../org/commcare/cases/entity/AsyncEntity.java | 16 ++++++++-------- .../cases/entity/AsyncNodeEntityFactory.java | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncEntity.java b/src/main/java/org/commcare/cases/entity/AsyncEntity.java index 05b7da55cf..d1fe8b2f98 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncEntity.java +++ b/src/main/java/org/commcare/cases/entity/AsyncEntity.java @@ -5,6 +5,7 @@ import static org.commcare.cases.entity.EntityStorageCache.ValueType.TYPE_SORT_FIELD; import org.commcare.cases.util.StringUtils; +import org.commcare.suite.model.Detail; import org.commcare.suite.model.DetailField; import org.commcare.suite.model.DetailGroup; import org.commcare.suite.model.Text; @@ -71,12 +72,11 @@ public class AsyncEntity extends Entity { */ private final Object mAsyncLock = new Object(); - public AsyncEntity(DetailField[] fields, EvaluationContext ec, - TreeReference t, Hashtable variables, - @Nullable EntityStorageCache cache, String cacheIndex, String detailId, - String extraKey, DetailGroup detailGroup, boolean cacheEnabled) { + public AsyncEntity(Detail detail, EvaluationContext ec, + TreeReference t, Hashtable variables, + @Nullable EntityStorageCache cache, String cacheIndex, String extraKey) { super(t, extraKey); - this.fields = fields; + this.fields = detail.getFields(); this.data = new Object[fields.length]; this.sortData = new String[fields.length]; this.sortDataPieces = new String[fields.length][]; @@ -90,9 +90,9 @@ public AsyncEntity(DetailField[] fields, EvaluationContext ec, //if this ref is _cachable_ every time, since it's a pretty big lift this.mCacheIndex = cacheIndex; - this.mDetailId = detailId; - this.mDetailGroup = detailGroup; - this.cacheEnabled = cacheEnabled; + this.mDetailId = detail.getId(); + this.mDetailGroup = detail.getGroup(); + this.cacheEnabled = detail.isCacheEnabled(); } private void loadVariableContext() { diff --git a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java index 62a3608b6c..ee2e04b9c8 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java @@ -56,8 +56,8 @@ public Entity getEntity(TreeReference data) { String entityKey = loadCalloutDataMapKey(nodeContext); AsyncEntity entity = - new AsyncEntity(detail.getFields(), nodeContext, data, mVariableDeclarations, - mEntityCache, mCacheIndex, detail.getId(), entityKey, detail.getGroup(), detail.isCacheEnabled()); + new AsyncEntity(detail, nodeContext, data, mVariableDeclarations, + mEntityCache, mCacheIndex, entityKey); if (mCacheIndex != null) { mEntitySet.put(mCacheIndex, entity); From b06d9ce347bc4ee486fd1452ef418ce24c160119 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 10 Jan 2025 13:36:35 +0530 Subject: [PATCH 319/402] Pre-calculate non lazy fields --- .../commcare/cases/entity/AsyncEntity.java | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncEntity.java b/src/main/java/org/commcare/cases/entity/AsyncEntity.java index d1fe8b2f98..04850b49fc 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncEntity.java +++ b/src/main/java/org/commcare/cases/entity/AsyncEntity.java @@ -43,7 +43,7 @@ public class AsyncEntity extends Entity { private final DetailField[] fields; private final Object[] data; private final String[] sortData; - private final boolean[] relevancyData; + private final Boolean[] relevancyData; private final String[][] sortDataPieces; private final String[] altTextData; @@ -51,7 +51,6 @@ public class AsyncEntity extends Entity { private final Hashtable mVariableDeclarations; private final DetailGroup mDetailGroup; private final boolean cacheEnabled; - private boolean mVariableContextLoaded = false; private final String mCacheIndex; private final String mDetailId; @@ -80,7 +79,7 @@ public AsyncEntity(Detail detail, EvaluationContext ec, this.data = new Object[fields.length]; this.sortData = new String[fields.length]; this.sortDataPieces = new String[fields.length][]; - this.relevancyData = new boolean[fields.length]; + this.relevancyData = new Boolean[fields.length]; this.altTextData = new String[fields.length]; this.context = ec; this.mVariableDeclarations = variables; @@ -93,6 +92,31 @@ public AsyncEntity(Detail detail, EvaluationContext ec, this.mDetailId = detail.getId(); this.mDetailGroup = detail.getGroup(); this.cacheEnabled = detail.isCacheEnabled(); + calculateNonLazyFields(detail); + } + + /** + * Calculates fields that are not lazy loaded as per following scenarios: + * 1. Both cache and lazy loading is not enabled i.e. we are in legacy cache and index => all fields are + * lazy loaded and we don't need to calculate anything here + * 2. only cache is enabled without lazy loading => calculate all fields now + * 3. both cache and lazy load are enabled => calculate all fields not marked as optimise + */ + private void calculateNonLazyFields(Detail detail) { + boolean lazyLoading = detail.isLazyLoading(); + if (!cacheEnabled && !lazyLoading) { + return; + } + + for (int i = 0; i < fields.length; i++) { + DetailField field = fields[i]; + if (!lazyLoading || !field.isOptimize()) { + data[i] = getField(i); + sortData[i] = getSortField(i); + altTextData[i] = getAltTextData(i); + relevancyData[i] = getRelevancyData(i); + } + } } private void loadVariableContext() { @@ -250,15 +274,24 @@ public boolean isValidField(int fieldIndex) { if (getField(fieldIndex).equals("")) { return false; } + return getRelevancyData(fieldIndex); + } + } + private boolean getRelevancyData(int i) { + synchronized (mAsyncLock) { + if (relevancyData[i] != null) { + return relevancyData[i]; + } + loadVariableContext(); try { - this.relevancyData[fieldIndex] = this.fields[fieldIndex].isRelevant(this.context); + relevancyData[i] = fields[i].isRelevant(context); } catch (XPathSyntaxException e) { - final String msg = "Invalid relevant condition for field : " + fields[fieldIndex].getHeader().toString(); + final String msg = "Invalid relevant condition for field : " + fields[i].getHeader().toString(); Logger.exception(msg, e); - throw new RuntimeException(msg); + throw new XPathException(e); } - return this.relevancyData[fieldIndex]; + return relevancyData[i]; } } @@ -333,6 +366,9 @@ public String getGroupKey() { @Nullable public String getAltTextData(int i) { synchronized (mAsyncLock) { + if (altTextData[i] != null) { + return altTextData[i]; + } loadVariableContext(); Text altText = fields[i].getAltText(); if (altText != null) { From 2ec92ada46b98e5f91c70bb769082e0bd71a131b Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 10 Jan 2025 16:32:25 +0530 Subject: [PATCH 320/402] lint and tests --- .../org/commcare/cases/entity/AsyncEntity.java | 3 --- .../cases/entity/AsyncNodeEntityFactory.java | 14 +++++++++----- .../entity/EntityLoadingProgressListener.java | 5 +++-- .../cases/entity/NodeEntityFactory.java | 18 ++++++++++++++++++ .../suite/model/test/AppStructureTests.java | 5 +++++ src/test/resources/app_structure/suite.xml | 15 +++++++++++++++ 6 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncEntity.java b/src/main/java/org/commcare/cases/entity/AsyncEntity.java index 04850b49fc..46872f5e8d 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncEntity.java +++ b/src/main/java/org/commcare/cases/entity/AsyncEntity.java @@ -177,7 +177,6 @@ private Object evaluateField(int i) { data[i] = fields[i].getTemplate().evaluate(context); } catch (XPathException xpe) { Logger.exception("Error while evaluating field for case list ", xpe); - xpe.printStackTrace(); data[i] = ""; } return data[i]; @@ -254,7 +253,6 @@ private void evaluateSortData(int i) { } } catch (XPathException xpe) { Logger.exception("Error while evaluating sort field", xpe); - xpe.printStackTrace(); sortData[i] = ""; } } @@ -376,7 +374,6 @@ public String getAltTextData(int i) { altTextData[i] = altText.evaluate(context); } catch (XPathException xpe) { Logger.exception("Error while evaluating field for case list ", xpe); - xpe.printStackTrace(); altTextData[i] = ""; } } diff --git a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java index ee2e04b9c8..212a167ba1 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java @@ -1,6 +1,8 @@ package org.commcare.cases.entity; +import static org.commcare.cases.entity.EntityLoadingProgressListener.EntityLoadingProgressPhase.PHASE_UNCACHED_CALCULATION; + import org.commcare.suite.model.Detail; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.TreeReference; @@ -140,12 +142,14 @@ protected void setUnCachedData(List> entities) { e.getSortField(col); } } - if (progressListener != null) { - progressListener.publishEntityLoadingProgress( - EntityLoadingProgressListener.EntityLoadingProgressPhase.PHASE_UNCACHED_CALCULATION, i, - entities.size()); + if (progressListener != null && i % 100 == 0) { + progressListener.publishEntityLoadingProgress(PHASE_UNCACHED_CALCULATION, i, entities.size()); } } + if (progressListener != null) { + progressListener.publishEntityLoadingProgress(PHASE_UNCACHED_CALCULATION, entities.size(), + entities.size()); + } } // Old cache and index pathway where we only cache sort fields @@ -159,7 +163,7 @@ protected void setUnCachedDataOld(List> entities) { } if (progressListener != null) { progressListener.publishEntityLoadingProgress( - EntityLoadingProgressListener.EntityLoadingProgressPhase.PHASE_UNCACHED_CALCULATION, i, + PHASE_UNCACHED_CALCULATION, i, entities.size()); } } diff --git a/src/main/java/org/commcare/cases/entity/EntityLoadingProgressListener.java b/src/main/java/org/commcare/cases/entity/EntityLoadingProgressListener.java index 73946efcaa..d30426f88c 100644 --- a/src/main/java/org/commcare/cases/entity/EntityLoadingProgressListener.java +++ b/src/main/java/org/commcare/cases/entity/EntityLoadingProgressListener.java @@ -19,9 +19,10 @@ enum EntityLoadingProgressPhase { */ PHASE_CACHING(2), + /** * Phase in which we calculate any uncached entity fields. Can take much longer if things are not - * already cached and similarly can be very quick when most things are arealdy cached. + * already cached and similarly can be very quick when most things are not available in cache. */ PHASE_UNCACHED_CALCULATION(3); @@ -49,7 +50,7 @@ public static EntityLoadingProgressPhase fromInt(int i) { * Method to implement to listen to the entity loading progress * * @param phase The specific phase of entity loading process - * @param progress progress corrosponding to the current entity loading phase + * @param progress progress corresponding to the current entity loading phase * @param total max progress corresponding to the current entity loading phase */ void publishEntityLoadingProgress(EntityLoadingProgressPhase phase, int progress, int total); diff --git a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java index 43557a9b56..fb6499792b 100755 --- a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java @@ -195,14 +195,32 @@ public final boolean isEntitySetReady() { } } + /** + * Caches the provided entities. Default implementation throws RuntimeException. + * Subclasses should override this method if they support caching. + * + * @param entities List of entities to cache + * @throws RuntimeException if caching is not supported + */ public void cacheEntities(List> entities) { throw new RuntimeException("Method not supported for normal Node Entity Factory"); } + /** + * Sets the progress listener for entity loading operations. + * + * @param progressListener The progress listener to use + */ public void setEntityProgressListener(EntityLoadingProgressListener progressListener) { this.progressListener = progressListener; } + /** + * Cancels the current loading operation. Default implementation throws RuntimeException. + * Subclasses should override this method if they support cancellation. + * + * @throws RuntimeException if cancellation is not supported + */ public void cancelLoading() { throw new RuntimeException("Method not supported for normal Node Entity Factory"); } diff --git a/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java b/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java index d836758fbf..6d94b6ca1e 100644 --- a/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java +++ b/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java @@ -261,6 +261,11 @@ public void testDetailPerformanceAttributes() { assertTrue(detail.isLazyLoading()); assertTrue(detail.isCacheEnabled()); assertTrue(detail.getFields()[0].isOptimize()); + assertFalse(detail.getFields()[1].isOptimize()); + + Detail detailNoCaching = mApp.getSession().getPlatform().getDetail("m1_case_short"); + assertFalse(detailNoCaching.isCacheEnabled()); + assertFalse(detailNoCaching.isCacheEnabled()); } @Test diff --git a/src/test/resources/app_structure/suite.xml b/src/test/resources/app_structure/suite.xml index 6eb45d9eaa..05da7051bd 100644 --- a/src/test/resources/app_structure/suite.xml +++ b/src/test/resources/app_structure/suite.xml @@ -94,6 +94,21 @@ + +
+ Name +
+ + + + + + +
From ac492df619a8ae735d13c37c85ddda1a39d23e6d Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Thu, 16 Jan 2025 16:49:33 +0530 Subject: [PATCH 321/402] -xpath implementation to calculate the distance from the boundary of the polygon --- .../core/model/utils/PolygonUtils.java | 121 ++++++++++++++++++ .../javarosa/xpath/expr/FunctionUtils.java | 1 + .../xpath/expr/XPathBoundaryDistanceFunc.java | 56 ++++++++ 3 files changed, 178 insertions(+) create mode 100644 src/main/java/org/javarosa/core/model/utils/PolygonUtils.java create mode 100644 src/main/java/org/javarosa/xpath/expr/XPathBoundaryDistanceFunc.java diff --git a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java new file mode 100644 index 0000000000..8a6f9e6317 --- /dev/null +++ b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java @@ -0,0 +1,121 @@ +package org.javarosa.core.model.utils; +import java.util.List; + +public class PolygonUtils { + + /** + * Determines if a point is inside a polygon. + * + * @param polygonPoints A list of doubles representing the polygon vertices + * (latitude and longitude pairs). + * @param testPoint A list of doubles representing the latitude and longitude of the test point. + * @return true if the point is inside the polygon, false otherwise. + */ + public static boolean isPointInsidePolygon(List<Double> polygonPoints, List<Double> testPoint) { + int intersectCount = 0; + int vertexCount = polygonPoints.size() / 2; + + double testLat = testPoint.get(0); + double testLng = testPoint.get(1); + + for (int i = 0; i < vertexCount; i++) { + double lat1 = polygonPoints.get(2 * i); + double lng1 = polygonPoints.get(2 * i + 1); + double lat2 = polygonPoints.get((2 * ((i + 1) % vertexCount))); + double lng2 = polygonPoints.get((2 * ((i + 1) % vertexCount)) + 1); + + if (rayIntersectsEdge(testLat, testLng, lat1, lng1, lat2, lng2)) { + intersectCount++; + } + } + + return (intersectCount % 2 == 1); + } + + /** + * Checks if a ray starting from the test point intersects the edge defined by two vertices. + */ + private static boolean rayIntersectsEdge(double testLat, double testLng, double lat1, double lng1, double lat2, double lng2) { + if (lat1 > lat2) { + double tempLat = lat1, tempLng = lng1; + lat1 = lat2; + lng1 = lng2; + lat2 = tempLat; + lng2 = tempLng; + } + + if (testLat < lat1 || testLat > lat2) { + return false; + } + + if (testLng > Math.max(lng1, lng2)) { + return false; + } + + if (testLng < Math.min(lng1, lng2)) { + return true; + } + + double slope = (lng2 - lng1) / (lat2 - lat1); + double intersectLng = lng1 + (testLat - lat1) * slope; + + return testLng < intersectLng; + } + + /** + * Calculates the distance from a point to the closest boundary of the polygon. + * + * @param polygonPoints A list of doubles representing the polygon vertices + * (latitude and longitude pairs). + * @param testPoint A list of doubles representing the latitude and longitude of the test point. + * @return The distance from the test point to the closest edge of the polygon. + */ + public static double distanceToClosestBoundary(List<Double> polygonPoints, double[] testPoint) { + double minDistance = Double.MAX_VALUE; + + int vertexCount = polygonPoints.size() / 2; + double testLat = testPoint[0]; + double testLng = testPoint[1]; + + for (int i = 0; i < vertexCount; i++) { + double lat1 = polygonPoints.get(2 * i); + double lng1 = polygonPoints.get(2 * i + 1); + double lat2 = polygonPoints.get((2 * ((i + 1) % vertexCount))); + double lng2 = polygonPoints.get((2 * ((i + 1) % vertexCount)) + 1); + + double distance = pointToSegmentDistance(testLat, testLng, lat1, lng1, lat2, lng2); + minDistance = Math.min(minDistance, distance); + } + + return minDistance; + } + + /** + * Calculates the shortest distance from a point to a line segment. + */ + private static double pointToSegmentDistance(double px, double py, double x1, double y1, double x2, double y2) { + double dx = x2 - x1; + double dy = y2 - y1; + + if (dx == 0 && dy == 0) { + // The segment is a point + return Math.sqrt(Math.pow(px - x1, 2) + Math.pow(py - y1, 2)); + } + + // Calculate the projection of the point onto the line + double t = ((px - x1) * dx + (py - y1) * dy) / (dx * dx + dy * dy); + + if (t < 0) { + // Closest to the first endpoint + return Math.sqrt(Math.pow(px - x1, 2) + Math.pow(py - y1, 2)); + } else if (t > 1) { + // Closest to the second endpoint + return Math.sqrt(Math.pow(px - x2, 2) + Math.pow(py - y2, 2)); + } else { + // Closest to a point on the segment + double projX = x1 + t * dx; + double projY = y1 + t * dy; + return Math.sqrt(Math.pow(px - projX, 2) + Math.pow(py - projY, 2)); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java b/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java index be30e0c724..03063fff21 100644 --- a/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java +++ b/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java @@ -90,6 +90,7 @@ public class FunctionUtils { funcList.put(XPathEncryptStringFunc.NAME, XPathEncryptStringFunc.class); funcList.put(XPathDecryptStringFunc.NAME, XPathDecryptStringFunc.class); funcList.put(XPathJsonPropertyFunc.NAME, XPathJsonPropertyFunc.class); + funcList.put(XPathBoundaryDistanceFunc.NAME, XPathBoundaryDistanceFunc.class); } private static final CacheTable<String, Double> mDoubleParseCache = new CacheTable<>(); diff --git a/src/main/java/org/javarosa/xpath/expr/XPathBoundaryDistanceFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathBoundaryDistanceFunc.java new file mode 100644 index 0000000000..8785714ec6 --- /dev/null +++ b/src/main/java/org/javarosa/xpath/expr/XPathBoundaryDistanceFunc.java @@ -0,0 +1,56 @@ +package org.javarosa.xpath.expr; + +import org.javarosa.core.model.condition.EvaluationContext; +import org.javarosa.core.model.data.GeoPointData; +import org.javarosa.core.model.data.UncastData; +import org.javarosa.core.model.instance.DataInstance; +import org.javarosa.core.model.utils.PolygonUtils; +import org.javarosa.xpath.XPathTypeMismatchException; +import org.javarosa.xpath.parser.XPathSyntaxException; + +import java.util.ArrayList; +import java.util.List; + +public class XPathBoundaryDistanceFunc extends XPathFuncExpr{ + public static final String NAME = "boundaryDistance"; + private static final int EXPECTED_ARG_COUNT = 2; + + public XPathBoundaryDistanceFunc() { + name = NAME; + expectedArgCount = EXPECTED_ARG_COUNT; + } + + public XPathBoundaryDistanceFunc(XPathExpression[] args) throws XPathSyntaxException { + super(NAME, args, EXPECTED_ARG_COUNT, true); + } + + + @Override + protected Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) { + return boundaryDistance(evaluatedArgs[0], evaluatedArgs[1]); + } + + public static Double boundaryDistance(Object from, Object to) { + String unpackedFrom = (String)FunctionUtils.unpack(from); + String unpackedTo = (String)FunctionUtils.unpack(to); + if (unpackedFrom == null || "".equals(unpackedFrom) || unpackedTo == null || "".equals(unpackedTo)) { + return Double.valueOf(-1.0); + } + try { + String[] coordinates=unpackedFrom.split(" "); + List<Double> polygonList = new ArrayList<Double>(); + + for (String coordinate : coordinates) { + polygonList.add(Double.parseDouble(coordinate)); + } + // Casting and uncasting seems strange but is consistent with the codebase + GeoPointData castedTo = new GeoPointData().cast(new UncastData(unpackedTo)); + double distance=PolygonUtils.distanceToClosestBoundary(polygonList,new double[]{castedTo.getLatitude(), castedTo.getLongitude()}); + + return distance; + } catch (NumberFormatException e) { + throw new XPathTypeMismatchException("distance() function requires arguments containing " + + "numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo); + } + } +} \ No newline at end of file From 43b1b619ac1ba14044ec4c841d5a040f1836a5ad Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Fri, 17 Jan 2025 01:55:47 +0530 Subject: [PATCH 322/402] -xpath implementation to calculate that the point is inside the polygon or not --- .../core/model/utils/PolygonUtils.java | 6 +- .../javarosa/xpath/expr/FunctionUtils.java | 3 +- .../xpath/expr/XPathPointInsidePolygon.java | 60 +++++++++++++++++++ ...unc.java => XPathPolygonDistanceFunc.java} | 15 +++-- .../xpath/parser/ast/ASTNodeFunctionCall.java | 6 ++ .../javarosa/xpath/test/XPathEvalTest.java | 2 +- 6 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java rename src/main/java/org/javarosa/xpath/expr/{XPathBoundaryDistanceFunc.java => XPathPolygonDistanceFunc.java} (77%) diff --git a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java index 8a6f9e6317..6b15749dcf 100644 --- a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java @@ -11,12 +11,12 @@ public class PolygonUtils { * @param testPoint A list of doubles representing the latitude and longitude of the test point. * @return true if the point is inside the polygon, false otherwise. */ - public static boolean isPointInsidePolygon(List<Double> polygonPoints, List<Double> testPoint) { + public static boolean isPointInsidePolygon(List<Double> polygonPoints, double[] testPoint) { int intersectCount = 0; int vertexCount = polygonPoints.size() / 2; - double testLat = testPoint.get(0); - double testLng = testPoint.get(1); + double testLat = testPoint[0]; + double testLng = testPoint[1]; for (int i = 0; i < vertexCount; i++) { double lat1 = polygonPoints.get(2 * i); diff --git a/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java b/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java index 03063fff21..d352c8ced4 100644 --- a/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java +++ b/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java @@ -90,7 +90,8 @@ public class FunctionUtils { funcList.put(XPathEncryptStringFunc.NAME, XPathEncryptStringFunc.class); funcList.put(XPathDecryptStringFunc.NAME, XPathDecryptStringFunc.class); funcList.put(XPathJsonPropertyFunc.NAME, XPathJsonPropertyFunc.class); - funcList.put(XPathBoundaryDistanceFunc.NAME, XPathBoundaryDistanceFunc.class); + funcList.put(XPathPolygonDistanceFunc.NAME, XPathPolygonDistanceFunc.class); + funcList.put(XPathPointInsidePolygon.NAME, XPathPointInsidePolygon.class); } private static final CacheTable<String, Double> mDoubleParseCache = new CacheTable<>(); diff --git a/src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java b/src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java new file mode 100644 index 0000000000..14b4582246 --- /dev/null +++ b/src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java @@ -0,0 +1,60 @@ +package org.javarosa.xpath.expr; + +import org.javarosa.core.model.condition.EvaluationContext; +import org.javarosa.core.model.data.GeoPointData; +import org.javarosa.core.model.data.UncastData; +import org.javarosa.core.model.instance.DataInstance; +import org.javarosa.core.model.utils.PolygonUtils; +import org.javarosa.xpath.XPathTypeMismatchException; +import org.javarosa.xpath.parser.XPathSyntaxException; +import java.util.ArrayList; +import java.util.List; + +public class XPathPointInsidePolygon extends XPathFuncExpr{ + public static final String NAME = "boundaryDistance"; + private static final int EXPECTED_ARG_COUNT = 2; + /** + * Returns true if the geopoint is inside the polygon, in meters, given objects to unpack. + * Ignores altitude and accuracy. + * Note that the arguments can be strings. + * Returns false if one of the arguments is null or the empty string. + */ + public XPathPointInsidePolygon() { + name = NAME; + expectedArgCount = EXPECTED_ARG_COUNT; + } + + public XPathPointInsidePolygon(XPathExpression[] args) throws XPathSyntaxException { + super(NAME, args, EXPECTED_ARG_COUNT, true); + } + + + @Override + protected Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) { + return boundaryDistance(evaluatedArgs[0], evaluatedArgs[1]); + } + + public static boolean boundaryDistance(Object from, Object to) { + String unpackedFrom = (String)FunctionUtils.unpack(from); + String unpackedTo = (String)FunctionUtils.unpack(to); + if (unpackedFrom == null || "".equals(unpackedFrom) || unpackedTo == null || "".equals(unpackedTo)) { + return false; + } + try { + String[] coordinates=unpackedFrom.split(" "); + List<Double> polygonList = new ArrayList<Double>(); + + for (String coordinate : coordinates) { + polygonList.add(Double.parseDouble(coordinate)); + } + // Casting and uncasting seems strange but is consistent with the codebase + GeoPointData castedTo = new GeoPointData().cast(new UncastData(unpackedTo)); + boolean isInside= PolygonUtils.isPointInsidePolygon(polygonList,new double[]{castedTo.getLatitude(), castedTo.getLongitude()}); + + return isInside; + } catch (NumberFormatException e) { + throw new XPathTypeMismatchException("point-in-boundary() function requires arguments containing " + + "numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/javarosa/xpath/expr/XPathBoundaryDistanceFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java similarity index 77% rename from src/main/java/org/javarosa/xpath/expr/XPathBoundaryDistanceFunc.java rename to src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java index 8785714ec6..095606817c 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathBoundaryDistanceFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java @@ -11,20 +11,25 @@ import java.util.ArrayList; import java.util.List; -public class XPathBoundaryDistanceFunc extends XPathFuncExpr{ +public class XPathPolygonDistanceFunc extends XPathFuncExpr{ public static final String NAME = "boundaryDistance"; private static final int EXPECTED_ARG_COUNT = 2; - public XPathBoundaryDistanceFunc() { + public XPathPolygonDistanceFunc() { name = NAME; expectedArgCount = EXPECTED_ARG_COUNT; } - public XPathBoundaryDistanceFunc(XPathExpression[] args) throws XPathSyntaxException { + public XPathPolygonDistanceFunc(XPathExpression[] args) throws XPathSyntaxException { super(NAME, args, EXPECTED_ARG_COUNT, true); } - + /** + * Returns the distance between the polygon and the geopoint, in meters, given objects to unpack. + * Ignores altitude and accuracy. + * Note that the arguments can be strings. + * Returns -1 if one of the arguments is null or the empty string. + */ @Override protected Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) { return boundaryDistance(evaluatedArgs[0], evaluatedArgs[1]); @@ -49,7 +54,7 @@ public static Double boundaryDistance(Object from, Object to) { return distance; } catch (NumberFormatException e) { - throw new XPathTypeMismatchException("distance() function requires arguments containing " + + throw new XPathTypeMismatchException("boundary-distance() function requires arguments containing " + "numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo); } } diff --git a/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java b/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java index 3527f9d406..b9d20e76ce 100755 --- a/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java +++ b/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java @@ -7,6 +7,7 @@ import org.javarosa.xpath.expr.XPathAtanTwoFunc; import org.javarosa.xpath.expr.XPathBooleanFromStringFunc; import org.javarosa.xpath.expr.XPathBooleanFunc; +import org.javarosa.xpath.expr.XPathPolygonDistanceFunc; import org.javarosa.xpath.expr.XPathCeilingFunc; import org.javarosa.xpath.expr.XPathChecklistFunc; import org.javarosa.xpath.expr.XPathChecksumFunc; @@ -48,6 +49,7 @@ import org.javarosa.xpath.expr.XPathNowFunc; import org.javarosa.xpath.expr.XPathNumberFunc; import org.javarosa.xpath.expr.XPathPiFunc; +import org.javarosa.xpath.expr.XPathPointInsidePolygon; import org.javarosa.xpath.expr.XPathPositionFunc; import org.javarosa.xpath.expr.XPathPowFunc; import org.javarosa.xpath.expr.XPathQName; @@ -257,6 +259,10 @@ private static XPathFuncExpr buildFuncExpr(String name, XPathExpression[] args) return new XPathDecryptStringFunc(args); case "json-property": return new XPathJsonPropertyFunc(args); + case "polygon-distance": + return new XPathPolygonDistanceFunc(args); + case "point-inside-polygon": + return new XPathPointInsidePolygon(args); default: return new XPathCustomRuntimeFunc(name, args); } diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index 24b69d2745..cf68a0ed5c 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.fail; -import org.commcare.util.EncryptionUtils; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.condition.IFunctionHandler; import org.javarosa.core.model.data.IAnswerData; @@ -174,6 +173,7 @@ public void doTests() { FormInstance instance = createTestInstance(); /* unsupporteds */ + testEval("boundary-distance('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.02052 27.20424')", null, null, Double.valueOf(0)); testEval("/union | /expr", null, null, new XPathUnsupportedException()); testEval("/descendant::blah", null, null, new XPathUnsupportedException()); testEval("/cant//support", null, null, new XPathUnsupportedException()); From f718a08d1948962b635204449cfa7e311d1f3378 Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Fri, 17 Jan 2025 02:41:01 +0530 Subject: [PATCH 323/402] -change name of the function file --- .../java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java b/src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java index 14b4582246..0060568f54 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java @@ -11,7 +11,7 @@ import java.util.List; public class XPathPointInsidePolygon extends XPathFuncExpr{ - public static final String NAME = "boundaryDistance"; + public static final String NAME = "insidePolygon"; private static final int EXPECTED_ARG_COUNT = 2; /** * Returns true if the geopoint is inside the polygon, in meters, given objects to unpack. From 74e90fb1152e4308876a6e43827cf7fad572018f Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Fri, 17 Jan 2025 03:03:49 +0530 Subject: [PATCH 324/402] -pr review requested --- src/main/java/org/javarosa/core/model/utils/PolygonUtils.java | 1 + src/test/java/org/javarosa/xpath/test/XPathEvalTest.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java index 6b15749dcf..5a103f6de8 100644 --- a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java @@ -10,6 +10,7 @@ public class PolygonUtils { * (latitude and longitude pairs). * @param testPoint A list of doubles representing the latitude and longitude of the test point. * @return true if the point is inside the polygon, false otherwise. + * This Code is written with the help of chatGPT */ public static boolean isPointInsidePolygon(List<Double> polygonPoints, double[] testPoint) { int intersectCount = 0; diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index cf68a0ed5c..66a7203062 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -173,7 +173,8 @@ public void doTests() { FormInstance instance = createTestInstance(); /* unsupporteds */ - testEval("boundary-distance('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.02052 27.20424')", null, null, Double.valueOf(0)); + testEval("polygon-distance('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.02052 27.20424')", null, null, Double.valueOf(0)); + testEval("point-inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.02052 27.20424')", null, null, false); testEval("/union | /expr", null, null, new XPathUnsupportedException()); testEval("/descendant::blah", null, null, new XPathUnsupportedException()); testEval("/cant//support", null, null, new XPathUnsupportedException()); From 7db32a663f1a4d16a2af8a7b03792ed0483c7ca3 Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Fri, 17 Jan 2025 03:40:27 +0530 Subject: [PATCH 325/402] -value to two decimal place of the distance --- .../java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java index 095606817c..1e7cdd54ff 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java @@ -52,7 +52,8 @@ public static Double boundaryDistance(Object from, Object to) { GeoPointData castedTo = new GeoPointData().cast(new UncastData(unpackedTo)); double distance=PolygonUtils.distanceToClosestBoundary(polygonList,new double[]{castedTo.getLatitude(), castedTo.getLongitude()}); - return distance; + + return Math.round(distance * 100.0) / 100.0; } catch (NumberFormatException e) { throw new XPathTypeMismatchException("boundary-distance() function requires arguments containing " + "numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo); From c20fe257f4a293e01a8cda6cc79cd6e073415453 Mon Sep 17 00:00:00 2001 From: Clayton Sims <csims@dimagi.com> Date: Thu, 16 Jan 2025 19:22:39 -0500 Subject: [PATCH 326/402] fix declared function name --- .../org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java index 1e7cdd54ff..27de6d3401 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java @@ -12,7 +12,7 @@ import java.util.List; public class XPathPolygonDistanceFunc extends XPathFuncExpr{ - public static final String NAME = "boundaryDistance"; + public static final String NAME = "polygon-distance"; private static final int EXPECTED_ARG_COUNT = 2; public XPathPolygonDistanceFunc() { @@ -59,4 +59,4 @@ public static Double boundaryDistance(Object from, Object to) { "numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo); } } -} \ No newline at end of file +} From 4041f13fb353bf2f37782a3307745ed70061b521 Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Fri, 17 Jan 2025 17:23:43 +0530 Subject: [PATCH 327/402] - bug fix and test cases for polygon test --- .../core/model/utils/PolygonUtils.java | 120 +++++++++--------- .../javarosa/xpath/expr/FunctionUtils.java | 2 +- ...java => XPathClosestPolygonPointFunc.java} | 25 ++-- .../xpath/expr/XPathPointInsidePolygon.java | 2 +- .../xpath/parser/ast/ASTNodeFunctionCall.java | 8 +- .../javarosa/xpath/test/XPathEvalTest.java | 11 +- 6 files changed, 87 insertions(+), 81 deletions(-) rename src/main/java/org/javarosa/xpath/expr/{XPathPolygonDistanceFunc.java => XPathClosestPolygonPointFunc.java} (66%) diff --git a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java index 5a103f6de8..61f99ed29f 100644 --- a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java @@ -10,7 +10,7 @@ public class PolygonUtils { * (latitude and longitude pairs). * @param testPoint A list of doubles representing the latitude and longitude of the test point. * @return true if the point is inside the polygon, false otherwise. - * This Code is written with the help of chatGPT + * This code is written with the help of chatgpt */ public static boolean isPointInsidePolygon(List<Double> polygonPoints, double[] testPoint) { int intersectCount = 0; @@ -33,6 +33,65 @@ public static boolean isPointInsidePolygon(List<Double> polygonPoints, double[] return (intersectCount % 2 == 1); } + /** + * Finds the minimum distance from a point to the polygon and the closest coordinate on the polygon. + * + * @param polygonPoints A list of doubles representing the polygon vertices + * (latitude and longitude pairs). + * @param testPoint A list of doubles representing the latitude and longitude of the test point. + * @return A result containing the minimum distance and the closest point on the polygon. + */ + public static String getClosestPoint(List<Double> polygonPoints, double[] testPoint) { + int numVertices = polygonPoints.size() / 2; + double[] closestPoint = null; + double minDistance = Double.MAX_VALUE; + + for (int i = 0; i < numVertices; i++) { + // Get the start and end points of the current edge + double startX = polygonPoints.get(2 * i); + double startY = polygonPoints.get(2 * i + 1); + double endX = polygonPoints.get(2 * ((i + 1) % numVertices)); + double endY = polygonPoints.get(2 * ((i + 1) % numVertices) + 1); + + // Find the closest point on this edge + double[] candidatePoint = getClosestPointOnSegment( + startX, startY, endX, endY, testPoint[0], testPoint[1]); + double distance = distanceBetween(candidatePoint, testPoint); + + // Update the closest point if necessary + if (distance < minDistance) { + minDistance = distance; + closestPoint = candidatePoint; + } + } + + // Return the closest point as a space-separated string + return closestPoint[0] + " " + closestPoint[1]; + } + + private static double[] getClosestPointOnSegment(double startX, double startY, double endX, double endY, double px, double py) { + double dx = endX - startX; + double dy = endY - startY; + + if (dx == 0 && dy == 0) { + // The segment is a single point + return new double[]{startX, startY}; + } + + // Calculate the projection factor t + double t = ((px - startX) * dx + (py - startY) * dy) / (dx * dx + dy * dy); + + // Clamp t to the range [0, 1] to stay on the segment + t = Math.max(0, Math.min(1, t)); + + // Compute the closest point + return new double[]{startX + t * dx, startY + t * dy}; + } + + private static double distanceBetween(double[] a, double[] b) { + return Math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1])); + } + /** * Checks if a ray starting from the test point intersects the edge defined by two vertices. */ @@ -62,61 +121,4 @@ private static boolean rayIntersectsEdge(double testLat, double testLng, double return testLng < intersectLng; } - - /** - * Calculates the distance from a point to the closest boundary of the polygon. - * - * @param polygonPoints A list of doubles representing the polygon vertices - * (latitude and longitude pairs). - * @param testPoint A list of doubles representing the latitude and longitude of the test point. - * @return The distance from the test point to the closest edge of the polygon. - */ - public static double distanceToClosestBoundary(List<Double> polygonPoints, double[] testPoint) { - double minDistance = Double.MAX_VALUE; - - int vertexCount = polygonPoints.size() / 2; - double testLat = testPoint[0]; - double testLng = testPoint[1]; - - for (int i = 0; i < vertexCount; i++) { - double lat1 = polygonPoints.get(2 * i); - double lng1 = polygonPoints.get(2 * i + 1); - double lat2 = polygonPoints.get((2 * ((i + 1) % vertexCount))); - double lng2 = polygonPoints.get((2 * ((i + 1) % vertexCount)) + 1); - - double distance = pointToSegmentDistance(testLat, testLng, lat1, lng1, lat2, lng2); - minDistance = Math.min(minDistance, distance); - } - - return minDistance; - } - - /** - * Calculates the shortest distance from a point to a line segment. - */ - private static double pointToSegmentDistance(double px, double py, double x1, double y1, double x2, double y2) { - double dx = x2 - x1; - double dy = y2 - y1; - - if (dx == 0 && dy == 0) { - // The segment is a point - return Math.sqrt(Math.pow(px - x1, 2) + Math.pow(py - y1, 2)); - } - - // Calculate the projection of the point onto the line - double t = ((px - x1) * dx + (py - y1) * dy) / (dx * dx + dy * dy); - - if (t < 0) { - // Closest to the first endpoint - return Math.sqrt(Math.pow(px - x1, 2) + Math.pow(py - y1, 2)); - } else if (t > 1) { - // Closest to the second endpoint - return Math.sqrt(Math.pow(px - x2, 2) + Math.pow(py - y2, 2)); - } else { - // Closest to a point on the segment - double projX = x1 + t * dx; - double projY = y1 + t * dy; - return Math.sqrt(Math.pow(px - projX, 2) + Math.pow(py - projY, 2)); - } - } -} \ No newline at end of file +} diff --git a/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java b/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java index d352c8ced4..3157fa470d 100644 --- a/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java +++ b/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java @@ -90,7 +90,7 @@ public class FunctionUtils { funcList.put(XPathEncryptStringFunc.NAME, XPathEncryptStringFunc.class); funcList.put(XPathDecryptStringFunc.NAME, XPathDecryptStringFunc.class); funcList.put(XPathJsonPropertyFunc.NAME, XPathJsonPropertyFunc.class); - funcList.put(XPathPolygonDistanceFunc.NAME, XPathPolygonDistanceFunc.class); + funcList.put(XPathClosestPolygonPointFunc.NAME, XPathClosestPolygonPointFunc.class); funcList.put(XPathPointInsidePolygon.NAME, XPathPointInsidePolygon.class); } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathClosestPolygonPointFunc.java similarity index 66% rename from src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java rename to src/main/java/org/javarosa/xpath/expr/XPathClosestPolygonPointFunc.java index 27de6d3401..10b18ef225 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathPolygonDistanceFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathClosestPolygonPointFunc.java @@ -11,35 +11,35 @@ import java.util.ArrayList; import java.util.List; -public class XPathPolygonDistanceFunc extends XPathFuncExpr{ - public static final String NAME = "polygon-distance"; +public class XPathClosestPolygonPointFunc extends XPathFuncExpr{ + public static final String NAME = "polygon-point"; private static final int EXPECTED_ARG_COUNT = 2; - public XPathPolygonDistanceFunc() { + public XPathClosestPolygonPointFunc() { name = NAME; expectedArgCount = EXPECTED_ARG_COUNT; } - public XPathPolygonDistanceFunc(XPathExpression[] args) throws XPathSyntaxException { + public XPathClosestPolygonPointFunc(XPathExpression[] args) throws XPathSyntaxException { super(NAME, args, EXPECTED_ARG_COUNT, true); } /** - * Returns the distance between the polygon and the geopoint, in meters, given objects to unpack. + * Returns the point on polygon closest to the geopoint, in "Lat Lng", given objects to unpack. * Ignores altitude and accuracy. * Note that the arguments can be strings. - * Returns -1 if one of the arguments is null or the empty string. + * Returns "" if one of the arguments is null or the empty string. */ @Override protected Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) { - return boundaryDistance(evaluatedArgs[0], evaluatedArgs[1]); + return closestPoint(evaluatedArgs[0], evaluatedArgs[1]); } - public static Double boundaryDistance(Object from, Object to) { + public static String closestPoint(Object from, Object to) { String unpackedFrom = (String)FunctionUtils.unpack(from); String unpackedTo = (String)FunctionUtils.unpack(to); if (unpackedFrom == null || "".equals(unpackedFrom) || unpackedTo == null || "".equals(unpackedTo)) { - return Double.valueOf(-1.0); + return ""; } try { String[] coordinates=unpackedFrom.split(" "); @@ -50,12 +50,11 @@ public static Double boundaryDistance(Object from, Object to) { } // Casting and uncasting seems strange but is consistent with the codebase GeoPointData castedTo = new GeoPointData().cast(new UncastData(unpackedTo)); - double distance=PolygonUtils.distanceToClosestBoundary(polygonList,new double[]{castedTo.getLatitude(), castedTo.getLongitude()}); + String closestPointResult=PolygonUtils.getClosestPoint(polygonList,new double[]{castedTo.getLatitude(), castedTo.getLongitude()}); - - return Math.round(distance * 100.0) / 100.0; + return closestPointResult; } catch (NumberFormatException e) { - throw new XPathTypeMismatchException("boundary-distance() function requires arguments containing " + + throw new XPathTypeMismatchException("polygon-point() function requires arguments containing " + "numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo); } } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java b/src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java index 0060568f54..feb7b8199a 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java @@ -11,7 +11,7 @@ import java.util.List; public class XPathPointInsidePolygon extends XPathFuncExpr{ - public static final String NAME = "insidePolygon"; + public static final String NAME = "inside-polygon"; private static final int EXPECTED_ARG_COUNT = 2; /** * Returns true if the geopoint is inside the polygon, in meters, given objects to unpack. diff --git a/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java b/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java index b9d20e76ce..24b6f23f64 100755 --- a/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java +++ b/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java @@ -7,7 +7,7 @@ import org.javarosa.xpath.expr.XPathAtanTwoFunc; import org.javarosa.xpath.expr.XPathBooleanFromStringFunc; import org.javarosa.xpath.expr.XPathBooleanFunc; -import org.javarosa.xpath.expr.XPathPolygonDistanceFunc; +import org.javarosa.xpath.expr.XPathClosestPolygonPointFunc; import org.javarosa.xpath.expr.XPathCeilingFunc; import org.javarosa.xpath.expr.XPathChecklistFunc; import org.javarosa.xpath.expr.XPathChecksumFunc; @@ -259,9 +259,9 @@ private static XPathFuncExpr buildFuncExpr(String name, XPathExpression[] args) return new XPathDecryptStringFunc(args); case "json-property": return new XPathJsonPropertyFunc(args); - case "polygon-distance": - return new XPathPolygonDistanceFunc(args); - case "point-inside-polygon": + case "polygon-point": + return new XPathClosestPolygonPointFunc(args); + case "inside-polygon": return new XPathPointInsidePolygon(args); default: return new XPathCustomRuntimeFunc(name, args); diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index 66a7203062..ca81349168 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -173,8 +173,6 @@ public void doTests() { FormInstance instance = createTestInstance(); /* unsupporteds */ - testEval("polygon-distance('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.02052 27.20424')", null, null, Double.valueOf(0)); - testEval("point-inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.02052 27.20424')", null, null, false); testEval("/union | /expr", null, null, new XPathUnsupportedException()); testEval("/descendant::blah", null, null, new XPathUnsupportedException()); testEval("/cant//support", null, null, new XPathUnsupportedException()); @@ -610,7 +608,14 @@ public void doTests() { testEval("$var_string_five", null, varContext, "five"); testEval("$var_int_five", null, varContext, Double.valueOf(5.0)); testEval("$var_double_five", null, varContext, Double.valueOf(5.0)); - + //Polygon point + testEval("polygon-point('78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78.041383 27.175569','78.041 27.176')", null, null, "78.041383 27.175569"); // Outside, near bottom-left vertex + + //inside polygon + testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.0195 27.204')", null, null, true); // Inside the polygon + testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.0205 27.2035')", null, null, false); // Outside the polygon, near bottom-right + testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.018 27.204')", null, null, false); // Outside the polygon, far left + testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.0187201 27.203509')", null, null, true); // On the polygon vertex //Attribute XPath References //testEval("/@blah", null, null, new XPathUnsupportedException()); //TODO: Need to test with model, probably in a different file From 0ca2de77a7d7f88c161a0b9172b992849bea77be Mon Sep 17 00:00:00 2001 From: Ahmad Treptt Vazirna <avazirna@Gmail.com> Date: Thu, 23 Jan 2025 14:54:27 +0200 Subject: [PATCH 328/402] Update version --- src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java b/src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java index 6112ecf551..697e00233a 100644 --- a/src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java +++ b/src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java @@ -70,7 +70,7 @@ public class CommCareConfigEngine { protected ArchiveFileRoot mArchiveRoot; public static final int MAJOR_VERSION = 2; - public static final int MINOR_VERSION = 53; + public static final int MINOR_VERSION = 55; public static final int MINIMAL_VERSION = 0; From 5b8b77bc057d7afae0e4ae15559daeb1b82898f6 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Thu, 30 Jan 2025 12:19:36 +0530 Subject: [PATCH 329/402] Abstract progress update in a separate method --- .../cases/entity/AsyncNodeEntityFactory.java | 28 ++++++++----------- .../entity/EntityLoadingProgressListener.java | 2 +- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java index 212a167ba1..803e00f31a 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java @@ -83,10 +83,6 @@ protected void setEvaluationContextDefaultQuerySet(EvaluationContext ec, */ protected void primeCache() { if (isCancelled) return; - if (progressListener != null) { - progressListener.publishEntityLoadingProgress( - EntityLoadingProgressListener.EntityLoadingProgressPhase.PHASE_CACHING, 0, 100); - } if (mEntityCache == null || mTemplateIsCachable == null || !mTemplateIsCachable || mCacheHost == null) { return; } @@ -95,10 +91,15 @@ protected void primeCache() { if (cachePrimeKeys == null) { return; } + updateProgress(EntityLoadingProgressListener.EntityLoadingProgressPhase.PHASE_CACHING, 0, 100); mEntityCache.primeCache(mEntitySet,cachePrimeKeys, detail); + updateProgress(EntityLoadingProgressListener.EntityLoadingProgressPhase.PHASE_CACHING, 100, 100); + } + + private void updateProgress(EntityLoadingProgressListener.EntityLoadingProgressPhase phase, int progress, + int total) { if (progressListener != null) { - progressListener.publishEntityLoadingProgress( - EntityLoadingProgressListener.EntityLoadingProgressPhase.PHASE_CACHING, 100, 100); + progressListener.publishEntityLoadingProgress(phase, progress, total); } } @@ -142,14 +143,11 @@ protected void setUnCachedData(List<Entity<TreeReference>> entities) { e.getSortField(col); } } - if (progressListener != null && i % 100 == 0) { - progressListener.publishEntityLoadingProgress(PHASE_UNCACHED_CALCULATION, i, entities.size()); + if (i % 100 == 0) { + updateProgress(PHASE_UNCACHED_CALCULATION, i, entities.size()); } } - if (progressListener != null) { - progressListener.publishEntityLoadingProgress(PHASE_UNCACHED_CALCULATION, entities.size(), - entities.size()); - } + updateProgress(PHASE_UNCACHED_CALCULATION, entities.size(), entities.size()); } // Old cache and index pathway where we only cache sort fields @@ -161,11 +159,7 @@ protected void setUnCachedDataOld(List<Entity<TreeReference>> entities) { for (int col = 0; col < e.getNumFields(); ++col) { e.getSortField(col); } - if (progressListener != null) { - progressListener.publishEntityLoadingProgress( - PHASE_UNCACHED_CALCULATION, i, - entities.size()); - } + updateProgress(PHASE_UNCACHED_CALCULATION, i, entities.size()); } } diff --git a/src/main/java/org/commcare/cases/entity/EntityLoadingProgressListener.java b/src/main/java/org/commcare/cases/entity/EntityLoadingProgressListener.java index d30426f88c..3886452193 100644 --- a/src/main/java/org/commcare/cases/entity/EntityLoadingProgressListener.java +++ b/src/main/java/org/commcare/cases/entity/EntityLoadingProgressListener.java @@ -22,7 +22,7 @@ enum EntityLoadingProgressPhase { /** * Phase in which we calculate any uncached entity fields. Can take much longer if things are not - * already cached and similarly can be very quick when most things are not available in cache. + * already cached and similarly can be very quick when most things are available in cache. */ PHASE_UNCACHED_CALCULATION(3); From 5a1422e017dec6897375e3c0f074982593f0d7dc Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Mon, 3 Feb 2025 15:00:04 +0530 Subject: [PATCH 330/402] Avoid overriding progress listener with an exception --- .../java/org/commcare/cases/entity/NodeEntityFactory.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java index fb6499792b..8100049704 100755 --- a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java @@ -212,6 +212,10 @@ public void cacheEntities(List<Entity<TreeReference>> entities) { * @param progressListener The progress listener to use */ public void setEntityProgressListener(EntityLoadingProgressListener progressListener) { + if (this.progressListener != null) { + throw new RuntimeException( + "Entity loading progress listener is already set in entity factory"); + } this.progressListener = progressListener; } From 7a37a3cea8f37b70267227561840f8f853a5d969 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Tue, 4 Feb 2025 11:41:19 +0530 Subject: [PATCH 331/402] move lazy loading related logic to factory method instead --- .../commcare/cases/entity/AsyncEntity.java | 25 ------------------- .../cases/entity/AsyncNodeEntityFactory.java | 16 +++++++++--- .../cases/entity/NodeEntityFactory.java | 5 ++-- 3 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncEntity.java b/src/main/java/org/commcare/cases/entity/AsyncEntity.java index 46872f5e8d..e91696d103 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncEntity.java +++ b/src/main/java/org/commcare/cases/entity/AsyncEntity.java @@ -92,31 +92,6 @@ public AsyncEntity(Detail detail, EvaluationContext ec, this.mDetailId = detail.getId(); this.mDetailGroup = detail.getGroup(); this.cacheEnabled = detail.isCacheEnabled(); - calculateNonLazyFields(detail); - } - - /** - * Calculates fields that are not lazy loaded as per following scenarios: - * 1. Both cache and lazy loading is not enabled i.e. we are in legacy cache and index => all fields are - * lazy loaded and we don't need to calculate anything here - * 2. only cache is enabled without lazy loading => calculate all fields now - * 3. both cache and lazy load are enabled => calculate all fields not marked as optimise - */ - private void calculateNonLazyFields(Detail detail) { - boolean lazyLoading = detail.isLazyLoading(); - if (!cacheEnabled && !lazyLoading) { - return; - } - - for (int i = 0; i < fields.length; i++) { - DetailField field = fields[i]; - if (!lazyLoading || !field.isOptimize()) { - data[i] = getField(i); - sortData[i] = getSortField(i); - altTextData[i] = getAltTextData(i); - relevancyData[i] = getRelevancyData(i); - } - } } private void loadVariableContext() { diff --git a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java index 803e00f31a..d09701a5a7 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java @@ -4,6 +4,7 @@ import static org.commcare.cases.entity.EntityLoadingProgressListener.EntityLoadingProgressPhase.PHASE_UNCACHED_CALCULATION; import org.commcare.suite.model.Detail; +import org.commcare.suite.model.DetailField; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.model.utils.CacheHost; @@ -123,22 +124,29 @@ protected void prepareEntitiesInternal(List<Entity<TreeReference>> entities) { } @Override - public void cacheEntities(List<Entity<TreeReference>> entities) { + public void cacheEntities(List<Entity<TreeReference>> entities, Boolean skipLazyLoad) { if (detail.isCacheEnabled()) { primeCache(); - setUnCachedData(entities); + setUnCachedData(entities, skipLazyLoad); } else { primeCache(); setUnCachedDataOld(entities); } } - protected void setUnCachedData(List<Entity<TreeReference>> entities) { + protected void setUnCachedData(List<Entity<TreeReference>> entities, Boolean skipLazyLoad) { for (int i = 0; i < entities.size(); i++) { if (isCancelled) return; AsyncEntity e = (AsyncEntity)entities.get(i); for (int col = 0; col < e.getNumFields(); ++col) { - if (detail.getFields()[col].isOptimize()) { + DetailField field = detail.getFields()[col]; + // if lazy loading, don't pre-calculate fields marked as optmize + if (!skipLazyLoad && detail.isLazyLoading()) { + if (!field.isOptimize()) { + e.getField(col); + e.getSortField(col); + } + } else { e.getField(col); e.getSortField(col); } diff --git a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java index 8100049704..84d463f4ed 100755 --- a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java @@ -199,10 +199,11 @@ public final boolean isEntitySetReady() { * Caches the provided entities. Default implementation throws RuntimeException. * Subclasses should override this method if they support caching. * - * @param entities List of entities to cache + * @param entities List of entities to cache + * @param skipLazyLoad true if we want to calculate even lazy-loading properties as part of the process * @throws RuntimeException if caching is not supported */ - public void cacheEntities(List<Entity<TreeReference>> entities) { + public void cacheEntities(List<Entity<TreeReference>> entities, Boolean skipLazyLoad) { throw new RuntimeException("Method not supported for normal Node Entity Factory"); } From 7769e9dd2aef1f3cf0b873f73a797f7bce6c2242 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Tue, 4 Feb 2025 15:46:32 +0530 Subject: [PATCH 332/402] rename skipLazyLoad -> inBackground --- .../cases/entity/AsyncNodeEntityFactory.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java index d09701a5a7..6757979f22 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java @@ -124,29 +124,33 @@ protected void prepareEntitiesInternal(List<Entity<TreeReference>> entities) { } @Override - public void cacheEntities(List<Entity<TreeReference>> entities, Boolean skipLazyLoad) { + public void cacheEntities(List<Entity<TreeReference>> entities, Boolean inBackground) { if (detail.isCacheEnabled()) { primeCache(); - setUnCachedData(entities, skipLazyLoad); + setUnCachedData(entities, inBackground); } else { primeCache(); setUnCachedDataOld(entities); } } - protected void setUnCachedData(List<Entity<TreeReference>> entities, Boolean skipLazyLoad) { + protected void setUnCachedData(List<Entity<TreeReference>> entities, Boolean inBackground) { for (int i = 0; i < entities.size(); i++) { if (isCancelled) return; AsyncEntity e = (AsyncEntity)entities.get(i); + boolean foregroundWithLazyLoading = !inBackground && detail.isLazyLoading(); + boolean foregroundWithoutLazyLoading = !inBackground && !detail.isLazyLoading(); for (int col = 0; col < e.getNumFields(); ++col) { DetailField field = detail.getFields()[col]; - // if lazy loading, don't pre-calculate fields marked as optmize - if (!skipLazyLoad && detail.isLazyLoading()) { - if (!field.isOptimize()) { - e.getField(col); - e.getSortField(col); - } - } else { + /** + * 1. If we are in foreground with lazy loading turned on, the priority is to show + * the user screen asap. Therefore, we need to skip calculating lazy fields. + * 2. If we are in foreground with lazy loading turned off, we want to calculate all fields here. + * 3. If we are in background with lazy loading turned on or off, we want to calculate all fields + * backed by cache in order to keep them ready for when user loads the list. + */ + if (foregroundWithoutLazyLoading || (foregroundWithLazyLoading && !field.isOptimize()) || ( + inBackground && field.isOptimize())) { e.getField(col); e.getSortField(col); } From 593146bc51368006fbc316f2cfde9ff274b693c3 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Mon, 10 Feb 2025 16:16:10 +0530 Subject: [PATCH 333/402] Shift to lazy loading and cache_enabled being defined on each field --- .../commcare/cases/entity/AsyncEntity.java | 4 +-- .../cases/entity/AsyncNodeEntityFactory.java | 4 +-- .../java/org/commcare/suite/model/Detail.java | 2 +- .../org/commcare/suite/model/DetailField.java | 25 +++++++++++++------ .../org/commcare/xml/DetailFieldParser.java | 6 +++-- .../suite/model/test/AppStructureTests.java | 6 +++-- src/test/resources/app_structure/suite.xml | 4 +-- 7 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncEntity.java b/src/main/java/org/commcare/cases/entity/AsyncEntity.java index e91696d103..0e71ee1ea5 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncEntity.java +++ b/src/main/java/org/commcare/cases/entity/AsyncEntity.java @@ -114,7 +114,7 @@ public Object getField(int i) { if (data[i] != null) { return data[i]; } - if (!fields[i].isOptimize()) { + if (!fields[i].isCacheEnabled()) { data[i] = evaluateField(i); return data[i]; } @@ -174,7 +174,7 @@ public String getSortField(int i) { } // eval and return if field is not marked as optimize - if (cacheEnabled && !fields[i].isOptimize()) { + if (cacheEnabled && !fields[i].isCacheEnabled()) { evaluateSortData(i); return sortData[i]; } diff --git a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java index 6757979f22..9f6ef2dc16 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java @@ -149,8 +149,8 @@ protected void setUnCachedData(List<Entity<TreeReference>> entities, Boolean inB * 3. If we are in background with lazy loading turned on or off, we want to calculate all fields * backed by cache in order to keep them ready for when user loads the list. */ - if (foregroundWithoutLazyLoading || (foregroundWithLazyLoading && !field.isOptimize()) || ( - inBackground && field.isOptimize())) { + if (foregroundWithoutLazyLoading || (foregroundWithLazyLoading && !field.isLazyLoading()) || ( + inBackground && field.isCacheEnabled())) { e.getField(col); e.getSortField(col); } diff --git a/src/main/java/org/commcare/suite/model/Detail.java b/src/main/java/org/commcare/suite/model/Detail.java index 84b95bd181..95be8e2f43 100755 --- a/src/main/java/org/commcare/suite/model/Detail.java +++ b/src/main/java/org/commcare/suite/model/Detail.java @@ -519,7 +519,7 @@ public boolean hasSortField() { public boolean shouldOptimize() { if (cacheEnabled || lazyLoading) { for (DetailField field : fields) { - if (field.isOptimize()) { + if (field.isCacheEnabled() || field.isLazyLoading()) { return true; } } diff --git a/src/main/java/org/commcare/suite/model/DetailField.java b/src/main/java/org/commcare/suite/model/DetailField.java index c9444fbc36..b8010710d4 100644 --- a/src/main/java/org/commcare/suite/model/DetailField.java +++ b/src/main/java/org/commcare/suite/model/DetailField.java @@ -77,7 +77,8 @@ public class DetailField implements Externalizable { private String fontSize; private String cssID; - private boolean optimize; + private boolean lazyLoading; + private boolean cacheEnabled; public DetailField() { } @@ -216,7 +217,8 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOExcep endpointAction = (EndpointAction)ExtUtil.read(in, new ExtWrapNullable(EndpointAction.class), pf); showBorder = ExtUtil.readBool(in); showShading = ExtUtil.readBool(in); - optimize = ExtUtil.readBool(in); + cacheEnabled = ExtUtil.readBool(in); + lazyLoading = ExtUtil.readBool(in); } @Override @@ -250,7 +252,8 @@ public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.write(out, new ExtWrapNullable(endpointAction)); ExtUtil.writeBool(out, showBorder); ExtUtil.writeBool(out, showShading); - ExtUtil.writeBool(out, optimize); + ExtUtil.writeBool(out, cacheEnabled); + ExtUtil.writeBool(out, lazyLoading); } public int getGridX() { @@ -303,8 +306,12 @@ public boolean getShowShading() { return showShading; } - public boolean isOptimize() { - return optimize; + public boolean isLazyLoading() { + return lazyLoading; + } + + public boolean isCacheEnabled() { + return cacheEnabled; } public static class Builder { @@ -443,8 +450,12 @@ public void setShowBorder(boolean showBorder) { public void setShowShading(boolean showShading) { field.showShading = showShading; } - public void setOptimize(boolean optimize) { - field.optimize = optimize; + public void setCacheEnabled(boolean cacheEnabled) { + field.cacheEnabled = cacheEnabled; + } + + public void setLazyLoading(boolean lazyLoading) { + field.lazyLoading = lazyLoading; } } } diff --git a/src/main/java/org/commcare/xml/DetailFieldParser.java b/src/main/java/org/commcare/xml/DetailFieldParser.java index aa4206d18e..b5b4d9ce9c 100644 --- a/src/main/java/org/commcare/xml/DetailFieldParser.java +++ b/src/main/java/org/commcare/xml/DetailFieldParser.java @@ -31,8 +31,10 @@ public DetailFieldParser(KXmlParser parser, GraphParser graphParser, String id) public DetailField parse() throws InvalidStructureException, IOException, XmlPullParserException { checkNode("field"); DetailField.Builder builder = new DetailField.Builder(); - boolean optimize = Boolean.parseBoolean(parser.getAttributeValue(null, "optimize")); - builder.setOptimize(optimize); + boolean cacheEnabled = Boolean.parseBoolean(parser.getAttributeValue(null, "cache_enabled")); + builder.setCacheEnabled(cacheEnabled); + boolean lazyLoading = Boolean.parseBoolean(parser.getAttributeValue(null, "lazy_loading")); + builder.setLazyLoading(lazyLoading); String sortDefault = parser.getAttributeValue(null, "sort"); if (sortDefault != null && sortDefault.equals("default")) { diff --git a/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java b/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java index 6d94b6ca1e..8ac8009d6c 100644 --- a/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java +++ b/src/test/java/org/commcare/backend/suite/model/test/AppStructureTests.java @@ -260,8 +260,10 @@ public void testDetailPerformanceAttributes() { Detail detail = mApp.getSession().getPlatform().getDetail("m0_case_short"); assertTrue(detail.isLazyLoading()); assertTrue(detail.isCacheEnabled()); - assertTrue(detail.getFields()[0].isOptimize()); - assertFalse(detail.getFields()[1].isOptimize()); + assertTrue(detail.getFields()[0].isCacheEnabled()); + assertFalse(detail.getFields()[0].isLazyLoading()); + assertTrue(detail.getFields()[1].isLazyLoading()); + assertFalse(detail.getFields()[1].isCacheEnabled()); Detail detailNoCaching = mApp.getSession().getPlatform().getDetail("m1_case_short"); assertFalse(detailNoCaching.isCacheEnabled()); diff --git a/src/test/resources/app_structure/suite.xml b/src/test/resources/app_structure/suite.xml index 05da7051bd..0a35f7eb86 100644 --- a/src/test/resources/app_structure/suite.xml +++ b/src/test/resources/app_structure/suite.xml @@ -66,7 +66,7 @@ </template> </field> </lookup> - <field optimize="true"> + <field cache_enabled="true"> <style horz-align="left" vert-align="center" font-size="small" show-border="true" show-shading="true"> <grid grid-height="2" grid-width="3" grid-x="3" grid-y="5"/> </style> @@ -94,7 +94,7 @@ </alt_text> <endpoint_action endpoint_id="case_list" background="true"/> </field> - <field> + <field lazy_loading="true"> <header> <text>Name</text> </header> From 073205487a678f944fda74223523f148990d42cc Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Thu, 27 Feb 2025 13:41:43 +0530 Subject: [PATCH 334/402] use primitive boolean --- .../org/commcare/cases/entity/AsyncNodeEntityFactory.java | 4 ++-- .../java/org/commcare/cases/entity/NodeEntityFactory.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java index 9f6ef2dc16..5b3f4cb180 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java @@ -124,7 +124,7 @@ protected void prepareEntitiesInternal(List<Entity<TreeReference>> entities) { } @Override - public void cacheEntities(List<Entity<TreeReference>> entities, Boolean inBackground) { + public void cacheEntities(List<Entity<TreeReference>> entities, boolean inBackground) { if (detail.isCacheEnabled()) { primeCache(); setUnCachedData(entities, inBackground); @@ -134,7 +134,7 @@ public void cacheEntities(List<Entity<TreeReference>> entities, Boolean inBackgr } } - protected void setUnCachedData(List<Entity<TreeReference>> entities, Boolean inBackground) { + protected void setUnCachedData(List<Entity<TreeReference>> entities, boolean inBackground) { for (int i = 0; i < entities.size(); i++) { if (isCancelled) return; AsyncEntity e = (AsyncEntity)entities.get(i); diff --git a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java index 84d463f4ed..30404cabad 100755 --- a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java @@ -203,7 +203,7 @@ public final boolean isEntitySetReady() { * @param skipLazyLoad true if we want to calculate even lazy-loading properties as part of the process * @throws RuntimeException if caching is not supported */ - public void cacheEntities(List<Entity<TreeReference>> entities, Boolean skipLazyLoad) { + public void cacheEntities(List<Entity<TreeReference>> entities, boolean skipLazyLoad) { throw new RuntimeException("Method not supported for normal Node Entity Factory"); } From 61d0a096f4679bc09d16739bf4f55fc03a4fb4ab Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Thu, 27 Feb 2025 15:52:25 +0530 Subject: [PATCH 335/402] Refactor inBackground as a constructor param --- .../commcare/util/screen/EntityScreenHelper.java | 4 +--- .../cases/entity/AsyncNodeEntityFactory.java | 15 +++++++++++---- .../commcare/cases/entity/NodeEntityFactory.java | 3 +-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java b/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java index b2764ff457..24b9186aab 100644 --- a/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreenHelper.java @@ -1,11 +1,9 @@ package org.commcare.util.screen; -import org.commcare.cases.entity.AsyncEntity; import org.commcare.cases.entity.AsyncNodeEntityFactory; import org.commcare.cases.entity.Entity; import org.commcare.cases.entity.EntitySortNotificationInterface; import org.commcare.cases.entity.EntitySorter; -import org.commcare.cases.entity.EntityStorageCache; import org.commcare.cases.entity.EntityStringFilterer; import org.commcare.cases.entity.NodeEntityFactory; import org.commcare.suite.model.Detail; @@ -37,7 +35,7 @@ public static List<Entity<TreeReference>> initEntities(EvaluationContext context EntityScreenContext entityScreenContext, TreeReference[] entitiesRefs) { NodeEntityFactory nodeEntityFactory; if (detail.isLazyLoading()) { - nodeEntityFactory = new AsyncNodeEntityFactory(detail, context, null); + nodeEntityFactory = new AsyncNodeEntityFactory(detail, context, null, false); } else { nodeEntityFactory = new NodeEntityFactory(detail, context); } diff --git a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java index 5b3f4cb180..ec8bec6ff5 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java @@ -35,12 +35,19 @@ public class AsyncNodeEntityFactory extends NodeEntityFactory { private final boolean isBlockingAsyncMode; private boolean isCancelled = false; + /** + * Whether we are loading entity in a background process. + * Used to accelerate processing in foreground by skipping lazy properties + */ + private boolean inBackground; + public AsyncNodeEntityFactory(Detail d, EvaluationContext ec, - @Nullable EntityStorageCache entityStorageCache) { + @Nullable EntityStorageCache entityStorageCache, boolean inBackground) { super(d, ec); mVariableDeclarations = detail.getVariableDeclarations(); mEntityCache = entityStorageCache; isBlockingAsyncMode = detail.hasSortField(); + this.inBackground = inBackground; } @Override @@ -124,17 +131,17 @@ protected void prepareEntitiesInternal(List<Entity<TreeReference>> entities) { } @Override - public void cacheEntities(List<Entity<TreeReference>> entities, boolean inBackground) { + public void cacheEntities(List<Entity<TreeReference>> entities) { if (detail.isCacheEnabled()) { primeCache(); - setUnCachedData(entities, inBackground); + setUnCachedData(entities); } else { primeCache(); setUnCachedDataOld(entities); } } - protected void setUnCachedData(List<Entity<TreeReference>> entities, boolean inBackground) { + protected void setUnCachedData(List<Entity<TreeReference>> entities) { for (int i = 0; i < entities.size(); i++) { if (isCancelled) return; AsyncEntity e = (AsyncEntity)entities.get(i); diff --git a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java index 30404cabad..23835af9f2 100755 --- a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java @@ -200,10 +200,9 @@ public final boolean isEntitySetReady() { * Subclasses should override this method if they support caching. * * @param entities List of entities to cache - * @param skipLazyLoad true if we want to calculate even lazy-loading properties as part of the process * @throws RuntimeException if caching is not supported */ - public void cacheEntities(List<Entity<TreeReference>> entities, boolean skipLazyLoad) { + public void cacheEntities(List<Entity<TreeReference>> entities) { throw new RuntimeException("Method not supported for normal Node Entity Factory"); } From 94b5d74371149854a8a4d58d7f7cd72e42228090 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Fri, 14 Feb 2025 17:44:59 +0530 Subject: [PATCH 336/402] Adds a method to get all related records from case graph --- src/main/java/org/javarosa/core/util/DAG.java | 31 + .../test/utilities/CasePurgeTest.java | 54 +- .../resources/case_relationship_tests.json | 664 ++++++++++++++++++ 3 files changed, 740 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/javarosa/core/util/DAG.java b/src/main/java/org/javarosa/core/util/DAG.java index 1752cfdf3a..15915d4769 100644 --- a/src/main/java/org/javarosa/core/util/DAG.java +++ b/src/main/java/org/javarosa/core/util/DAG.java @@ -1,8 +1,12 @@ package org.javarosa.core.util; +import org.commcare.modern.util.Pair; + import java.util.Enumeration; import java.util.HashSet; import java.util.Hashtable; +import java.util.LinkedList; +import java.util.Set; import java.util.Stack; import java.util.Vector; @@ -138,6 +142,33 @@ public Stack<I> getSinks() { return roots; } + public Set<I> getRelatedRecords(Set<I> recordIds) { + Set<I> visited = new HashSet<>(); + LinkedList<I> queue = new LinkedList<>(recordIds); + while (!queue.isEmpty()) { + I current = queue.poll(); + if(visited.contains(current)){ + continue; + } + visited.add(current); + addNeighbors(edges, current, queue, visited); + addNeighbors(inverseEdges, current, queue, visited); + } + return visited; + } + + private void addNeighbors(Hashtable<I, Vector<Edge<I, E>>> edges, I current, LinkedList<I> queue, + Set<I> visited) { + if (edges.containsKey(current)) { + Vector<Edge<I, E>> neighbors = edges.get(current); + for (Edge<I, E> neighbor : neighbors) { + if(!visited.contains(neighbor.i)){ + queue.add(neighbor.i); + } + } + } + } + public static class Edge<I, E> { public final I i; public final E e; diff --git a/src/test/java/org/commcare/test/utilities/CasePurgeTest.java b/src/test/java/org/commcare/test/utilities/CasePurgeTest.java index 3c90e76b31..5a262b8d94 100644 --- a/src/test/java/org/commcare/test/utilities/CasePurgeTest.java +++ b/src/test/java/org/commcare/test/utilities/CasePurgeTest.java @@ -1,11 +1,16 @@ package org.commcare.test.utilities; +import static org.commcare.cases.util.CasePurgeFilter.getFullCaseGraph; + +import com.google.errorprone.annotations.Immutable; + import org.commcare.cases.model.Case; import org.commcare.cases.model.CaseIndex; import org.commcare.cases.util.CasePurgeFilter; import org.commcare.cases.util.InvalidCaseGraphException; import org.javarosa.core.services.storage.IStorageIterator; import org.javarosa.core.services.storage.util.DummyIndexedStorageUtility; +import org.javarosa.core.util.DAG; import org.javarosa.core.util.externalizable.LivePrototypeFactory; import org.json.JSONArray; import org.json.JSONException; @@ -17,8 +22,10 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.Vector; /** @@ -52,11 +59,11 @@ public static Iterable<Object[]> testData() { } private static Object[] parseParametersFromJSONObject(JSONObject root) { - Object[] parameters = new Object[7]; + Object[] parameters = new Object[8]; parameters[0] = root.getString("name"); String[] jsonArrayKeys = - new String[]{"cases", "owned", "closed", "subcases", "extensions", "outcome"}; + new String[]{"cases", "owned", "closed", "subcases", "extensions", "outcome", "relation_outcome"}; for (int i = 0; i < jsonArrayKeys.length; i++) { addJSONArrayIfPresent(root, i+1, jsonArrayKeys[i], parameters); } @@ -76,17 +83,19 @@ private static void addJSONArrayIfPresent(JSONObject root, int index, String key private final HashSet<String> ownedCases = new HashSet<>(); private final HashSet<String> closedCases = new HashSet<>(); private final HashSet<String> outcomeSet = new HashSet<>(); + private final HashMap<String, HashSet<String>> relationOutcomeSet = new HashMap<>(); private final ArrayList<String[]> indices = new ArrayList<>(); public CasePurgeTest(String name, JSONArray cases, JSONArray owned, JSONArray closed, - JSONArray subcases, JSONArray extensions, JSONArray outcome) { + JSONArray subcases, JSONArray extensions, JSONArray outcome, JSONArray relationOutcomes) { this.name = name; - createTestObjectsFromParameters(cases, owned, closed, subcases, extensions, outcome); + createTestObjectsFromParameters(cases, owned, closed, subcases, extensions, outcome, relationOutcomes); } private void createTestObjectsFromParameters(JSONArray casesJson, JSONArray ownedJson, - JSONArray closedJson, JSONArray subcasesJson, - JSONArray extensionsJson, JSONArray outcomeJson) { + JSONArray closedJson, JSONArray subcasesJson, + JSONArray extensionsJson, JSONArray outcomeJson, + JSONArray relationOutcomes) { if (casesJson != null) { getCases(casesJson, cases); } @@ -104,6 +113,23 @@ private void createTestObjectsFromParameters(JSONArray casesJson, JSONArray owne getIndices(extensionsJson, indices, CaseIndex.RELATIONSHIP_EXTENSION); } getCases(outcomeJson, outcomeSet); + if (relationOutcomes != null) { + populateRelationOutcomes(relationOutcomes, outcomeSet); + } + } + + private void populateRelationOutcomes(JSONArray relationOutcomes, HashSet<String> outcomeSet) + throws JSONException { + int count = 0; + for (String outcome : outcomeSet) { + JSONObject relationOutcome = (JSONObject)relationOutcomes.get(count++); + JSONArray relatedCases = relationOutcome.optJSONArray("related_cases"); + HashSet<String> relatedCasesSet = new HashSet<>(); + for (int i = 0; i < relatedCases.length(); ++i) { + relatedCasesSet.add(relatedCases.getString(i)); + } + relationOutcomeSet.put(outcome, relatedCasesSet); + } } private void getIndices(JSONArray indices, ArrayList<String[]> indexSet, @@ -138,16 +164,26 @@ public void executeTest() throws InvalidCaseGraphException { Vector<String> ownerIds = new Vector<>(); ownerIds.add(userId); - storage.removeAll(new CasePurgeFilter(storage, ownerIds)); + + storage.removeAll(new CasePurgeFilter(getFullCaseGraph(storage, ownerIds))); HashSet<String> inStorage = new HashSet<>(); + // redo the graph as we don't want the eliminated cases anymore + DAG<String, int[], String> graph = getFullCaseGraph(storage, ownerIds); for (IStorageIterator<Case> iterator = storage.iterate(); iterator.hasMore(); ) { Case c = iterator.nextRecord(); - inStorage.add(c.getCaseId()); + String caseId = c.getCaseId(); + inStorage.add(caseId); + + + HashSet<String> relatedCasesSet = relationOutcomeSet.get(caseId); + HashSet<String> input = new HashSet<>(); + input.add(caseId); + Set<String> relatedCases = graph.getRelatedRecords(input); + Assert.assertEquals(name, relatedCasesSet, relatedCases); } Assert.assertEquals(name, outcomeSet, inStorage); - } private void initCaseStorage(DummyIndexedStorageUtility<Case> storage, diff --git a/src/test/resources/case_relationship_tests.json b/src/test/resources/case_relationship_tests.json index 37d84aab01..2806bf73b3 100644 --- a/src/test/resources/case_relationship_tests.json +++ b/src/test/resources/case_relationship_tests.json @@ -22,6 +22,23 @@ "a", "b", "c" + ], + "relation_outcome": [ + { + "related_cases": [ + "a" + ] + }, + { + "related_cases": [ + "b" + ] + }, + { + "related_cases": [ + "c" + ] + } ] }, { @@ -38,6 +55,20 @@ "outcome": [ "d", "b" + ], + "relation_outcome": [ + { + "related_cases": [ + "d", + "b" + ] + }, + { + "related_cases": [ + "b", + "d" + ] + } ] }, { @@ -65,6 +96,20 @@ "outcome": [ "a", "b" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "b" + ] + }, + { + "related_cases": [ + "b", + "a" + ] + } ] }, { @@ -84,6 +129,13 @@ ], "outcome": [ "a" + ], + "relation_outcome": [ + { + "related_cases": [ + "a" + ] + } ] }, { @@ -127,6 +179,20 @@ "outcome": [ "d", "b" + ], + "relation_outcome": [ + { + "related_cases": [ + "d", + "b" + ] + }, + { + "related_cases": [ + "b", + "d" + ] + } ] }, { @@ -152,6 +218,13 @@ ], "outcome": [ "b" + ], + "relation_outcome": [ + { + "related_cases": [ + "b" + ] + } ] }, { @@ -168,6 +241,20 @@ "outcome": [ "a", "e" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "e" + ] + }, + { + "related_cases": [ + "e", + "a" + ] + } ] }, { @@ -211,6 +298,29 @@ "a", "b", "e" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "b", + "e" + ] + }, + { + "related_cases": [ + "b", + "e", + "a" + ] + }, + { + "related_cases": [ + "e", + "a", + "b" + ] + } ] }, { @@ -263,6 +373,29 @@ "a", "b", "e" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "b", + "e" + ] + }, + { + "related_cases": [ + "b", + "e", + "a" + ] + }, + { + "related_cases": [ + "e", + "a", + "b" + ] + } ] }, { @@ -288,6 +421,13 @@ ], "outcome": [ "b" + ], + "relation_outcome": [ + { + "related_cases": [ + "b" + ] + } ] }, { @@ -304,6 +444,20 @@ "outcome": [ "d", "a" + ], + "relation_outcome": [ + { + "related_cases": [ + "d", + "a" + ] + }, + { + "related_cases": [ + "a", + "d" + ] + } ] }, { @@ -320,6 +474,20 @@ "outcome": [ "d", "a" + ], + "relation_outcome": [ + { + "related_cases": [ + "d", + "a" + ] + }, + { + "related_cases": [ + "a", + "d" + ] + } ] }, { @@ -341,6 +509,29 @@ "d", "a", "b" + ], + "relation_outcome": [ + { + "related_cases": [ + "d", + "a", + "b" + ] + }, + { + "related_cases": [ + "a", + "d", + "b" + ] + }, + { + "related_cases": [ + "a", + "d", + "b" + ] + } ] }, { @@ -367,6 +558,40 @@ "b", "c", "d" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "b", + "c", + "d" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d" + ] + } ] }, { @@ -400,6 +625,53 @@ "c", "d", "e" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d", + "e" + ] + } ] }, { @@ -418,6 +690,13 @@ ], "outcome": [ "a" + ], + "relation_outcome": [ + { + "related_cases": [ + "a" + ] + } ] }, { @@ -444,6 +723,29 @@ "a", "b", "c" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "b", + "c" + ] + }, + { + "related_cases": [ + "a", + "b", + "c" + ] + }, + { + "related_cases": [ + "a", + "b", + "c" + ] + } ] }, { @@ -471,6 +773,29 @@ "a", "b", "c" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "b", + "c" + ] + }, + { + "related_cases": [ + "a", + "b", + "c" + ] + }, + { + "related_cases": [ + "a", + "b", + "c" + ] + } ] }, { @@ -497,6 +822,20 @@ "outcome": [ "a", "b" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "b" + ] + }, + { + "related_cases": [ + "a", + "b" + ] + } ] }, { @@ -521,6 +860,29 @@ "a", "b", "e" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "b", + "e" + ] + }, + { + "related_cases": [ + "a", + "b", + "e" + ] + }, + { + "related_cases": [ + "a", + "b", + "e" + ] + } ] }, { @@ -655,6 +1017,13 @@ ], "outcome": [ "a" + ], + "relation_outcome": [ + { + "related_cases": [ + "a" + ] + } ] }, { @@ -687,6 +1056,53 @@ "c", "d", "e" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d", + "e" + ] + } ] }, { @@ -721,6 +1137,13 @@ ], "outcome": [ "a" + ], + "relation_outcome": [ + { + "related_cases": [ + "a" + ] + } ] }, { @@ -756,6 +1179,29 @@ ], "outcome": [ "a", "b", "c" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "b", + "c" + ] + }, + { + "related_cases": [ + "a", + "b", + "c" + ] + }, + { + "related_cases": [ + "a", + "b", + "c" + ] + } ] }, { @@ -773,6 +1219,13 @@ ], "outcome": [ "a" + ], + "relation_outcome": [ + { + "related_cases": [ + "a" + ] + } ] }, { @@ -791,6 +1244,13 @@ ], "outcome": [ "a" + ], + "relation_outcome": [ + { + "related_cases": [ + "a" + ] + } ] }, { @@ -808,6 +1268,40 @@ ], "outcome": [ "d", "a", "c", "e" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "c", + "d", + "e" + ] + }, + { + "related_cases": [ + "a", + "c", + "d", + "e" + ] + }, + { + "related_cases": [ + "a", + "c", + "d", + "e" + ] + }, + { + "related_cases": [ + "a", + "c", + "d", + "e" + ] + } ] }, { @@ -828,6 +1322,40 @@ ], "outcome": [ "a", "b", "c", "d" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "b", + "c", + "d" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d" + ] + }, + { + "related_cases": [ + "a", + "b", + "c", + "d" + ] + } ] }, { @@ -848,6 +1376,20 @@ ], "outcome": [ "a", "b" + ], + "relation_outcome": [ + { + "related_cases": [ + "a", + "b" + ] + }, + { + "related_cases": [ + "a", + "b" + ] + } ] }, { @@ -871,6 +1413,38 @@ ], "outcome": [ "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ], + "relation_outcome": [ + { + "related_cases": [ + "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ] + }, + { + "related_cases": [ + "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ] + }, + { + "related_cases": [ + "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ] + }, + { + "related_cases": [ + "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ] + }, + { + "related_cases": [ + "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ] + }, + { + "related_cases": [ + "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ] + } ] }, { @@ -894,6 +1468,38 @@ ], "outcome": [ "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ], + "relation_outcome": [ + { + "related_cases": [ + "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ] + }, + { + "related_cases": [ + "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ] + }, + { + "related_cases": [ + "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ] + }, + { + "related_cases": [ + "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ] + }, + { + "related_cases": [ + "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ] + }, + { + "related_cases": [ + "claim", "house", "p_mother", "p_child", "child_health", "measurement" + ] + } ] }, { @@ -918,6 +1524,18 @@ ], "outcome": [ "claim", "house" + ], + "relation_outcome": [ + { + "related_cases": [ + "claim", "house" + ] + }, + { + "related_cases": [ + "claim", "house" + ] + } ] }, { @@ -942,6 +1560,18 @@ ], "outcome": [ "claim", "house" + ], + "relation_outcome": [ + { + "related_cases": [ + "claim", "house" + ] + }, + { + "related_cases": [ + "claim", "house" + ] + } ] }, { @@ -963,6 +1593,33 @@ ], "outcome": [ "L", "B", "C", "D", "E" + ], + "relation_outcome": [ + { + "related_cases": [ + "L", "B", "C", "D", "E" + ] + }, + { + "related_cases": [ + "L", "B", "C", "D", "E" + ] + }, + { + "related_cases": [ + "L", "B", "C", "D", "E" + ] + }, + { + "related_cases": [ + "L", "B", "C", "D", "E" + ] + }, + { + "related_cases": [ + "L", "B", "C", "D", "E" + ] + } ] }, { @@ -978,6 +1635,13 @@ ], "outcome": [ "parent" + ], + "relation_outcome": [ + { + "related_cases": [ + "parent" + ] + } ] } From 24da04f8e80bb90348f69698c9826ba0a2063f48 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Mon, 3 Mar 2025 13:09:38 +0530 Subject: [PATCH 337/402] Better method names and docstring --- src/main/java/org/javarosa/core/util/DAG.java | 19 +++++++++++++------ .../test/utilities/CasePurgeTest.java | 4 +--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/javarosa/core/util/DAG.java b/src/main/java/org/javarosa/core/util/DAG.java index 15915d4769..56300e718a 100644 --- a/src/main/java/org/javarosa/core/util/DAG.java +++ b/src/main/java/org/javarosa/core/util/DAG.java @@ -1,7 +1,5 @@ package org.javarosa.core.util; -import org.commcare.modern.util.Pair; - import java.util.Enumeration; import java.util.HashSet; import java.util.Hashtable; @@ -142,7 +140,15 @@ public Stack<I> getSinks() { return roots; } - public Set<I> getRelatedRecords(Set<I> recordIds) { + /** + * Performs a breadth-first search (BFS) to find all related records + * starting from the given set of record IDs by traversing both + * forward and inverse edges. + * + * @param recordIds The set of starting record IDs. + * @return A set containing all reachable records. + */ + public Set<I> findConnectedRecords(Set<I> recordIds) { Set<I> visited = new HashSet<>(); LinkedList<I> queue = new LinkedList<>(recordIds); while (!queue.isEmpty()) { @@ -151,13 +157,14 @@ public Set<I> getRelatedRecords(Set<I> recordIds) { continue; } visited.add(current); - addNeighbors(edges, current, queue, visited); - addNeighbors(inverseEdges, current, queue, visited); + enqueueUnvisitedNeighbors(edges, current, queue, visited); + enqueueUnvisitedNeighbors(inverseEdges, current, queue, visited); } return visited; } - private void addNeighbors(Hashtable<I, Vector<Edge<I, E>>> edges, I current, LinkedList<I> queue, + // Adds unvisited neighboring nodes of the given record to the queue for further traversal + private void enqueueUnvisitedNeighbors(Hashtable<I, Vector<Edge<I, E>>> edges, I current, LinkedList<I> queue, Set<I> visited) { if (edges.containsKey(current)) { Vector<Edge<I, E>> neighbors = edges.get(current); diff --git a/src/test/java/org/commcare/test/utilities/CasePurgeTest.java b/src/test/java/org/commcare/test/utilities/CasePurgeTest.java index 5a262b8d94..5a6e5717e5 100644 --- a/src/test/java/org/commcare/test/utilities/CasePurgeTest.java +++ b/src/test/java/org/commcare/test/utilities/CasePurgeTest.java @@ -2,8 +2,6 @@ import static org.commcare.cases.util.CasePurgeFilter.getFullCaseGraph; -import com.google.errorprone.annotations.Immutable; - import org.commcare.cases.model.Case; import org.commcare.cases.model.CaseIndex; import org.commcare.cases.util.CasePurgeFilter; @@ -179,7 +177,7 @@ public void executeTest() throws InvalidCaseGraphException { HashSet<String> relatedCasesSet = relationOutcomeSet.get(caseId); HashSet<String> input = new HashSet<>(); input.add(caseId); - Set<String> relatedCases = graph.getRelatedRecords(input); + Set<String> relatedCases = graph.findConnectedRecords(input); Assert.assertEquals(name, relatedCasesSet, relatedCases); } From 4f97f683e3ef7ef3b2939cb81aea203c33af2ae2 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Tue, 4 Mar 2025 15:39:54 +0530 Subject: [PATCH 338/402] Adds getBulkIdsForIndex --- .../core/services/storage/IStorageUtilityIndexed.java | 10 ++++++++++ .../storage/util/DummyIndexedStorageUtility.java | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java b/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java index 9fc30ba7cb..a4d865b920 100644 --- a/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java +++ b/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java @@ -279,6 +279,16 @@ void bulkRead(LinkedHashSet<Integer> cuedCases, HashMap<Integer, E> recordMap) */ Vector<E> getBulkRecordsForIndex(String metaFieldName, Collection<String> matchingValues); + + /** + * Bulk retrieves a set of the record ids in storage based on a list of values matching one of the + * field for this storage + * @param metaFieldName field we are matching against + * @param matchingValues matching values for metaFieldName that we want to filter records against + * @return A Vector of Externalizable objects e, such that the field specified is equal to the corresponding value provided. + */ + Vector<Integer> getBulkIdsForIndex(String metaFieldName, Collection<String> matchingValues); + /** * Provide public accessor to the inner class that is stored */ diff --git a/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java b/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java index 33874b4d9f..c319abc856 100644 --- a/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java +++ b/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java @@ -367,6 +367,13 @@ public Vector<T> getBulkRecordsForIndex(String metaFieldName, Collection<String> return getRecordsForValues(new String[]{metaFieldName}, matchingValues.toArray()); } + @Override + public Vector<Integer> getBulkIdsForIndex(String metaFieldName, Collection<String> matchingValues) { + // we don't care about bulk retrieval for dummy storage, so just call normal method to get records here + List<Integer> result = getIDsForValues(new String[]{metaFieldName}, matchingValues.toArray()); + return new Vector<>(result); + } + @Override public void bulkReadMetadata(LinkedHashSet cuedCases, String[] metaDataIds, HashMap metadataMap) { for (int i : ((LinkedHashSet<Integer>)cuedCases)) { From 6d9ea681d2ec5f58edaaa3de11b0c7d1e7df99a0 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Tue, 4 Mar 2025 16:33:13 +0530 Subject: [PATCH 339/402] better docstring and naming --- src/main/java/org/javarosa/core/util/DAG.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/javarosa/core/util/DAG.java b/src/main/java/org/javarosa/core/util/DAG.java index 56300e718a..b6c75b4312 100644 --- a/src/main/java/org/javarosa/core/util/DAG.java +++ b/src/main/java/org/javarosa/core/util/DAG.java @@ -141,16 +141,14 @@ public Stack<I> getSinks() { } /** - * Performs a breadth-first search (BFS) to find all related records - * starting from the given set of record IDs by traversing both - * forward and inverse edges. + * Find all nodes reachable from the given set of source nodes * - * @param recordIds The set of starting record IDs. - * @return A set containing all reachable records. + * @param sourceNodes The set of starting nodes + * @return A set containing all reachable nodes */ - public Set<I> findConnectedRecords(Set<I> recordIds) { + public Set<I> findConnectedRecords(Set<I> sourceNodes) { Set<I> visited = new HashSet<>(); - LinkedList<I> queue = new LinkedList<>(recordIds); + LinkedList<I> queue = new LinkedList<>(sourceNodes); while (!queue.isEmpty()) { I current = queue.poll(); if(visited.contains(current)){ From de8b0d6b31131180e150036415649a69d7245631 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Tue, 11 Mar 2025 14:58:32 +0530 Subject: [PATCH 340/402] Adds a method to check for entity caching on app level --- .../org/commcare/util/CommCarePlatform.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/main/java/org/commcare/util/CommCarePlatform.java b/src/main/java/org/commcare/util/CommCarePlatform.java index fae9888858..9099d23a32 100644 --- a/src/main/java/org/commcare/util/CommCarePlatform.java +++ b/src/main/java/org/commcare/util/CommCarePlatform.java @@ -4,11 +4,13 @@ import org.commcare.resources.model.ResourceTable; import org.commcare.suite.model.Detail; import org.commcare.suite.model.Endpoint; +import org.commcare.suite.model.EntityDatum; import org.commcare.suite.model.Entry; import org.commcare.suite.model.FormEntry; import org.commcare.suite.model.Menu; import org.commcare.suite.model.OfflineUserRestore; import org.commcare.suite.model.Profile; +import org.commcare.suite.model.SessionDatum; import org.commcare.suite.model.Suite; import org.javarosa.core.model.instance.FormInstance; import org.javarosa.core.services.PropertyManager; @@ -20,6 +22,7 @@ import java.util.Enumeration; import java.util.Hashtable; import java.util.List; +import java.util.Map; import java.util.Vector; import javax.annotation.Nullable; @@ -215,6 +218,33 @@ public String getMenuDisplayStyle(String menuId) { return commonDisplayStyle; } + + /** + * Loops through complete set of detail config and checks whether + * any of them are cache enabled + * + * @return true if entity caching is enabled for any of the detail configs in app, false otherwise + */ + public boolean isEntityCachingEnabled() { + Map<String, Entry> commandMap = getCommandToEntryMap(); + for (String command : commandMap.keySet()) { + Entry entry = commandMap.get(command); + if (entry == null) continue; + for (SessionDatum sessionDatum : entry.getSessionDataReqs()) { + if (sessionDatum instanceof EntityDatum) { + String shortDetailId = ((EntityDatum)sessionDatum).getShortDetail(); + if (shortDetailId != null) { + Detail detail = getDetail(shortDetailId); + if (detail.isCacheEnabled()) { + return true; + } + } + } + } + } + return false; + } + public OfflineUserRestore getDemoUserRestore() { return offlineUserRestore; } From a8fd89c6d5014fe98d536ca5403774023e3c4e83 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Tue, 11 Mar 2025 16:45:25 +0530 Subject: [PATCH 341/402] move cancellation flag to base factory as it can be potentially useful for normal loading as well --- .../cases/entity/AsyncNodeEntityFactory.java | 7 ------- .../commcare/cases/entity/NodeEntityFactory.java | 16 ++++++++-------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java index ec8bec6ff5..63d9ed9c44 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java @@ -19,7 +19,6 @@ * @author ctsims */ public class AsyncNodeEntityFactory extends NodeEntityFactory { - private static final String TAG = AsyncNodeEntityFactory.class.getSimpleName(); private final OrderedHashtable<String, XPathExpression> mVariableDeclarations; private final Hashtable<String, AsyncEntity> mEntitySet = new Hashtable<>(); @@ -33,7 +32,6 @@ public class AsyncNodeEntityFactory extends NodeEntityFactory { // Don't show entity list until we primeCache and caches all fields private final boolean isBlockingAsyncMode; - private boolean isCancelled = false; /** * Whether we are loading entity in a background process. @@ -189,11 +187,6 @@ protected boolean isEntitySetReadyInternal() { } } - @Override - public void cancelLoading() { - isCancelled = true; - } - public boolean isBlockingAsyncMode() { return isBlockingAsyncMode; } diff --git a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java index 23835af9f2..2fc9fb2aed 100755 --- a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java @@ -27,6 +27,12 @@ public class NodeEntityFactory { private ReducingTraceReporter traceReporter; protected EntityLoadingProgressListener progressListener; + /** + * Flag that denotes cancellation of the underlying process responsible for loading entities + * Implementations of long running methods can check this to interrupt and exit early + */ + protected boolean isCancelled = false; + public NodeEntityFactory(Detail d, EvaluationContext ec) { this.detail = d; this.ec = ec; @@ -219,13 +225,7 @@ public void setEntityProgressListener(EntityLoadingProgressListener progressList this.progressListener = progressListener; } - /** - * Cancels the current loading operation. Default implementation throws RuntimeException. - * Subclasses should override this method if they support cancellation. - * - * @throws RuntimeException if cancellation is not supported - */ - public void cancelLoading() { - throw new RuntimeException("Method not supported for normal Node Entity Factory"); + public void markAsCancelled() { + isCancelled = true; } } From 418fa88f6cf13c3169d22b32db638715f0ba90a4 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Wed, 12 Mar 2025 16:24:14 +0530 Subject: [PATCH 342/402] move boolean checks out of loop --- .../org/commcare/cases/entity/AsyncNodeEntityFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java index 63d9ed9c44..55f1181d1c 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java @@ -140,11 +140,11 @@ public void cacheEntities(List<Entity<TreeReference>> entities) { } protected void setUnCachedData(List<Entity<TreeReference>> entities) { + boolean foregroundWithLazyLoading = !inBackground && detail.isLazyLoading(); + boolean foregroundWithoutLazyLoading = !inBackground && !detail.isLazyLoading(); for (int i = 0; i < entities.size(); i++) { if (isCancelled) return; AsyncEntity e = (AsyncEntity)entities.get(i); - boolean foregroundWithLazyLoading = !inBackground && detail.isLazyLoading(); - boolean foregroundWithoutLazyLoading = !inBackground && !detail.isLazyLoading(); for (int col = 0; col < e.getNumFields(); ++col) { DetailField field = detail.getFields()[col]; /** From d80c2db3143948a9eb83c93d22050ef572a0a893 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Thu, 13 Mar 2025 12:06:47 +0530 Subject: [PATCH 343/402] Adds volatile --- src/main/java/org/commcare/cases/entity/NodeEntityFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java index 2fc9fb2aed..9e25eb81ca 100755 --- a/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/NodeEntityFactory.java @@ -31,7 +31,7 @@ public class NodeEntityFactory { * Flag that denotes cancellation of the underlying process responsible for loading entities * Implementations of long running methods can check this to interrupt and exit early */ - protected boolean isCancelled = false; + protected volatile boolean isCancelled = false; public NodeEntityFactory(Detail d, EvaluationContext ec) { this.detail = d; From 56d9e2b0762d9c782619ba20e31a101765bd935e Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Thu, 10 Apr 2025 17:15:52 +0530 Subject: [PATCH 344/402] Only calculate the sort field from cache if the field defines the sort block --- .../org/commcare/cases/entity/AsyncNodeEntityFactory.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java index 55f1181d1c..dac78b721f 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java +++ b/src/main/java/org/commcare/cases/entity/AsyncNodeEntityFactory.java @@ -157,7 +157,9 @@ protected void setUnCachedData(List<Entity<TreeReference>> entities) { if (foregroundWithoutLazyLoading || (foregroundWithLazyLoading && !field.isLazyLoading()) || ( inBackground && field.isCacheEnabled())) { e.getField(col); - e.getSortField(col); + if (field.getSort() != null) { + e.getSortField(col); + } } } if (i % 100 == 0) { From 87f5ea0cc3655ce81997f81aa6e6da91353b4130 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna <avazirna@gmail.com> Date: Mon, 14 Apr 2025 12:16:29 +0200 Subject: [PATCH 345/402] Bump JSON to version 20250107 --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 44258c5aab..fa00c68b32 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ dependencies { api 'com.github.stefanhaustein:kxml2:2.4.1' implementation 'javax.ws.rs:javax.ws.rs-api:2.0.1' // Added so Android Studio recognizes libs in util jar projects - implementation 'org.json:json:20220924' + implementation 'org.json:json:20250107' implementation 'commons-cli:commons-cli:1.3.1' implementation 'joda-time:joda-time:2.12.1' implementation 'com.carrotsearch:hppc:0.9.1' @@ -84,7 +84,7 @@ dependencies { implementation 'io.opentracing:opentracing-util:0.33.0' implementation 'com.datadoghq:dd-trace-api:1.10.0' - cliImplementation 'org.json:json:20140107' + cliImplementation 'org.json:json:20250107' cliImplementation 'com.github.stefanhaustein:kxml2:2.4.1' cliImplementation 'commons-cli:commons-cli:1.3.1' cliImplementation 'joda-time:joda-time:2.9.4' @@ -99,7 +99,7 @@ dependencies { testImplementation sourceSets.cli.output testImplementation sourceSets.translate.output testImplementation 'junit:junit:4.13.2' - testImplementation 'org.json:json:20140107' + testImplementation 'org.json:json:20250107' testImplementation 'org.xerial:sqlite-jdbc:3.40.0.0' testImplementation 'joda-time:joda-time:2.9.4' testImplementation 'com.squareup.retrofit2:retrofit:2.3.0' @@ -107,7 +107,7 @@ dependencies { testImplementation 'com.google.guava:guava:31.1-jre' ccapiImplementation sourceSets.main.output - ccapiImplementation 'org.json:json:20140107' + ccapiImplementation 'org.json:json:20250107' ccapiImplementation 'org.xerial:sqlite-jdbc:3.40.0.0' ccapiImplementation 'com.github.stefanhaustein:kxml2:2.4.1' ccapiImplementation 'joda-time:joda-time:2.9.4' @@ -125,7 +125,7 @@ dependencies { translateImplementation 'commons-cli:commons-cli:1.3.1' translateImplementation files('lib/json-simple-1.1.1.jar') translateImplementation 'joda-time:joda-time:2.9.4' - translateImplementation 'org.json:json:20140107' + translateImplementation 'org.json:json:20250107' translateImplementation 'io.reactivex.rxjava2:rxjava:2.1.1' translateImplementation "com.squareup.retrofit2:retrofit:2.3.0" translateImplementation 'com.squareup.okhttp3:okhttp:4.11.0' From ed6da9096a8277e6bab0c09ff6f9fede411b2568 Mon Sep 17 00:00:00 2001 From: Minha <mlee@dimagi.com> Date: Thu, 27 Mar 2025 16:31:13 -0400 Subject: [PATCH 346/402] Add Document control type --- src/main/java/org/javarosa/core/model/Constants.java | 1 + src/main/java/org/javarosa/xform/parse/XFormParser.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/main/java/org/javarosa/core/model/Constants.java b/src/main/java/org/javarosa/core/model/Constants.java index bef7a70592..d122274d2c 100755 --- a/src/main/java/org/javarosa/core/model/Constants.java +++ b/src/main/java/org/javarosa/core/model/Constants.java @@ -79,6 +79,7 @@ public class Constants { public static final int CONTROL_LABEL = 11; public static final int CONTROL_AUDIO_CAPTURE = 12; public static final int CONTROL_VIDEO_CAPTURE = 13; + public static final int CONTROL_DOCUMENT_UPLOAD = 14; /** * constants for xform tags diff --git a/src/main/java/org/javarosa/xform/parse/XFormParser.java b/src/main/java/org/javarosa/xform/parse/XFormParser.java index a94098838c..5c1cc4340f 100755 --- a/src/main/java/org/javarosa/xform/parse/XFormParser.java +++ b/src/main/java/org/javarosa/xform/parse/XFormParser.java @@ -898,6 +898,8 @@ protected QuestionDef parseUpload(IFormElement parent, Element e, int controlUpl question.setControlType(Constants.CONTROL_AUDIO_CAPTURE); } else if ("video/*".equals(mediaType)) { question.setControlType(Constants.CONTROL_VIDEO_CAPTURE); + } else if ("application/*,text/*".equals(mediaType)) { + question.setControlType(Constants.CONTROL_DOCUMENT_UPLOAD); } return question; From 3ca12d4c58ee790d7c318843d65c5009569d7f0c Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Tue, 29 Apr 2025 23:19:35 +0530 Subject: [PATCH 347/402] Bump platform version --- src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java b/src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java index 697e00233a..69646c9a2a 100644 --- a/src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java +++ b/src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java @@ -70,7 +70,7 @@ public class CommCareConfigEngine { protected ArchiveFileRoot mArchiveRoot; public static final int MAJOR_VERSION = 2; - public static final int MINOR_VERSION = 55; + public static final int MINOR_VERSION = 57; public static final int MINIMAL_VERSION = 0; From cb779d18c5dd1d083d2689079fcc682243915d59 Mon Sep 17 00:00:00 2001 From: David Viggiano <dave.viggiano@gmail.com> Date: Thu, 1 May 2025 01:43:20 -0400 Subject: [PATCH 348/402] Refactored encryption code so it all happens in the core version of EncryptionUtils. Deleted Android version of EncryptionUtils. --- .../org/commcare/util/EncryptionUtils.java | 140 +++++++++++++----- 1 file changed, 103 insertions(+), 37 deletions(-) diff --git a/src/main/java/org/commcare/util/EncryptionUtils.java b/src/main/java/org/commcare/util/EncryptionUtils.java index cb317cb313..7cca6425e9 100644 --- a/src/main/java/org/commcare/util/EncryptionUtils.java +++ b/src/main/java/org/commcare/util/EncryptionUtils.java @@ -4,6 +4,8 @@ import java.nio.charset.Charset; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; +import java.security.Key; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import javax.crypto.BadPaddingException; @@ -17,44 +19,62 @@ public class EncryptionUtils { - /** - * Encrypts a message using the AES encryption and produces a base64 encoded payload containing the ciphertext, and a random IV which was used to encrypt the input. - * - * @param message a UTF-8 encoded message to be encrypted - * @param key A base64 encoded 256 bit symmetric key - * @return A base64 encoded payload containing the IV and AES encrypted ciphertext, which can be decoded by this utility's decrypt method and the same symmetric key - */ - public static String encrypt(String message, String key) throws EncryptionException { - final String ENCRYPT_ALGO = "AES/GCM/NoPadding"; + public static String encrypt(byte[] message, Key key, String transform, + boolean includeMessageLength) throws EncryptionException { final int MIN_IV_LENGTH_BYTE = 1; final int MAX_IV_LENGTH_BYTE = 255; - SecretKey secret = getSecretKeySpec(key); try { - Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); - cipher.init(Cipher.ENCRYPT_MODE, secret); - byte[] encryptedMessage = cipher.doFinal(message.getBytes(Charset.forName("UTF-8"))); + Cipher cipher = Cipher.getInstance(transform); + cipher.init(Cipher.ENCRYPT_MODE, key); + byte[] encryptedMessage = cipher.doFinal(message); byte[] iv = cipher.getIV(); if (iv.length < MIN_IV_LENGTH_BYTE || iv.length > MAX_IV_LENGTH_BYTE) { throw new EncryptionException("Initialization vector should be between " + MIN_IV_LENGTH_BYTE + " and " + MAX_IV_LENGTH_BYTE + " bytes long, but it is " + iv.length + " bytes"); } + + + int extraBytes = includeMessageLength ? 2 : 0; + // The conversion of iv.length to byte takes the low 8 bits. To // convert back, cast to int and mask with 0xFF. - byte[] ivPlusMessage = ByteBuffer.allocate(1 + iv.length + encryptedMessage.length) - .put((byte)iv.length) - .put(iv) - .put(encryptedMessage) - .array(); - return Base64.encode(ivPlusMessage); - } catch (Exception ex) { - throw new EncryptionException("Unknown error during encryption", ex); + ByteBuffer byteBuffer = ByteBuffer.allocate(1 + iv.length + extraBytes + encryptedMessage.length) + .put((byte) iv.length) + .put(iv); + + if(includeMessageLength) { + byteBuffer.put((byte)(encryptedMessage.length / 256)); + byteBuffer.put((byte)(encryptedMessage.length % 256)); + } + + byteBuffer.put(encryptedMessage); + + return Base64.encode(byteBuffer.array()); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | + BadPaddingException | IllegalBlockSizeException e) { + throw new EncryptionException("Error during encryption", e); } } + /** + * Encrypts a message using the AES encryption and produces a base64 encoded payload containing the ciphertext, and a random IV which was used to encrypt the input. + * + * @param message a UTF-8 encoded message to be encrypted + * @param key A base64 encoded 256 bit symmetric key + * @return A base64 encoded payload containing the IV and AES encrypted ciphertext, which can be decoded by this utility's decrypt method and the same symmetric key + */ + public static String encrypt(String message, String key) throws EncryptionException { + final String ENCRYPT_ALGO = "AES/GCM/NoPadding"; + SecretKey secret = getSecretKeySpec(key); + + return encrypt(message.getBytes(Charset.forName("UTF-8")), + secret, ENCRYPT_ALGO, false); + } + - private static SecretKey getSecretKeySpec(String key) throws EncryptionException { + public static SecretKey getSecretKeySpec(String key) throws EncryptionException { final int KEY_LENGTH_BIT = 256; byte[] keyBytes; try { @@ -69,6 +89,53 @@ private static SecretKey getSecretKeySpec(String key) throws EncryptionException return new SecretKeySpec(keyBytes, "AES"); } + public static byte[] decrypt(byte[] bytes, Key key, String keyAndTransform, + boolean includeMessageLength) + throws EncryptionException { + final int TAG_LENGTH_BIT = 128; + int readIndex = 0; + int ivLength = bytes[readIndex] & 0xFF; + readIndex++; + if (ivLength < 0) { + //Note: Early chance to catch decryption error + throw new EncryptionException("Negative IV length"); + } + byte[] iv = null; + if (ivLength > 0) { + iv = new byte[ivLength]; + System.arraycopy(bytes, readIndex, iv, 0, ivLength); + readIndex += ivLength; + } + + int encryptedLength; + if(includeMessageLength) { + encryptedLength= (bytes[readIndex] & 0xFF) << 8; + readIndex++; + encryptedLength += (bytes[readIndex] & 0xFF); + } else { + encryptedLength = bytes.length - readIndex; + } + + byte[] encrypted = new byte[encryptedLength]; + readIndex++; + System.arraycopy(bytes, readIndex, encrypted, 0, encryptedLength); + + try { + Cipher cipher = Cipher.getInstance(keyAndTransform); + + if (includeMessageLength) { + cipher.init(Cipher.DECRYPT_MODE, key, iv != null ? new IvParameterSpec(iv) : null); + } else { + cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); + } + + return cipher.doFinal(encrypted); + } catch(NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | + InvalidAlgorithmParameterException | BadPaddingException | InvalidKeyException e) { + throw new RuntimeException(e); + } + } + /** * Decrypts a base64 payload containing an IV and AES encrypted ciphertext using the provided key * @@ -78,27 +145,26 @@ private static SecretKey getSecretKeySpec(String key) throws EncryptionException */ public static String decrypt(String message, String key) throws EncryptionException { final String ENCRYPT_ALGO = "AES/GCM/NoPadding"; - final int TAG_LENGTH_BIT = 128; SecretKey secret = getSecretKeySpec(key); try { byte[] messageBytes = Base64.decode(message); - ByteBuffer bb = ByteBuffer.wrap(messageBytes); - int iv_length_byte = bb.get() & 0xFF; - byte[] iv = new byte[iv_length_byte]; - bb.get(iv); + byte[] plainText = decrypt(messageBytes, secret, ENCRYPT_ALGO, false); - byte[] cipherText = new byte[bb.remaining()]; - bb.get(cipherText); - - - Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); - cipher.init(Cipher.DECRYPT_MODE, secret, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); - byte[] plainText = cipher.doFinal(cipherText); return new String(plainText, Charset.forName("UTF-8")); - } catch (NoSuchAlgorithmException | BadPaddingException | NoSuchPaddingException | - IllegalBlockSizeException | InvalidAlgorithmParameterException | InvalidKeyException | Base64DecoderException e) { - throw new EncryptionException("Error encountered while decrypting the message", e); + } catch(Base64DecoderException e) { + throw new EncryptionException("Decrypting message failed", e); + } + } + + public static String getMd5HashAsString(String plainText) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(plainText.getBytes()); + byte[] hashInBytes = md.digest(); + return Base64.encode(hashInBytes); + } catch (NoSuchAlgorithmException e) { + return ""; } } From d19b2a92b6146441f6443e658a95baeaa50a2949 Mon Sep 17 00:00:00 2001 From: David Viggiano <dave.viggiano@gmail.com> Date: Thu, 1 May 2025 02:12:00 -0400 Subject: [PATCH 349/402] Fixed encryption issue (incrementing readIndex at wrong time). Added some null checks. --- src/main/java/org/commcare/util/EncryptionUtils.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/util/EncryptionUtils.java b/src/main/java/org/commcare/util/EncryptionUtils.java index 7cca6425e9..8c635c6fe4 100644 --- a/src/main/java/org/commcare/util/EncryptionUtils.java +++ b/src/main/java/org/commcare/util/EncryptionUtils.java @@ -24,6 +24,14 @@ public static String encrypt(byte[] message, Key key, String transform, final int MIN_IV_LENGTH_BYTE = 1; final int MAX_IV_LENGTH_BYTE = 255; + if(key == null) { + throw new NullPointerException("Key is null"); + } + + if(transform == null) { + throw new NullPointerException("Transform is null"); + } + try { Cipher cipher = Cipher.getInstance(transform); cipher.init(Cipher.ENCRYPT_MODE, key); @@ -117,7 +125,6 @@ public static byte[] decrypt(byte[] bytes, Key key, String keyAndTransform, } byte[] encrypted = new byte[encryptedLength]; - readIndex++; System.arraycopy(bytes, readIndex, encrypted, 0, encryptedLength); try { From d585f73b6459037438cf44ba2cb551d1179ff6fe Mon Sep 17 00:00:00 2001 From: David Viggiano <dave.viggiano@gmail.com> Date: Thu, 1 May 2025 02:57:17 -0400 Subject: [PATCH 350/402] Supporting empty IV (when using RSA) --- .../org/commcare/util/EncryptionUtils.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/commcare/util/EncryptionUtils.java b/src/main/java/org/commcare/util/EncryptionUtils.java index 8c635c6fe4..dfd9a256d9 100644 --- a/src/main/java/org/commcare/util/EncryptionUtils.java +++ b/src/main/java/org/commcare/util/EncryptionUtils.java @@ -32,25 +32,30 @@ public static String encrypt(byte[] message, Key key, String transform, throw new NullPointerException("Transform is null"); } + boolean allowEmptyIV = transform.startsWith("RSA"); + try { Cipher cipher = Cipher.getInstance(transform); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] encryptedMessage = cipher.doFinal(message); byte[] iv = cipher.getIV(); - if (iv.length < MIN_IV_LENGTH_BYTE || iv.length > MAX_IV_LENGTH_BYTE) { + int ivLength = iv == null ? 0 : iv.length; + if (!allowEmptyIV && (ivLength < MIN_IV_LENGTH_BYTE || ivLength > MAX_IV_LENGTH_BYTE)) { throw new EncryptionException("Initialization vector should be between " + MIN_IV_LENGTH_BYTE + " and " + MAX_IV_LENGTH_BYTE + - " bytes long, but it is " + iv.length + " bytes"); + " bytes long, but it is " + ivLength + " bytes"); } - int extraBytes = includeMessageLength ? 2 : 0; // The conversion of iv.length to byte takes the low 8 bits. To // convert back, cast to int and mask with 0xFF. - ByteBuffer byteBuffer = ByteBuffer.allocate(1 + iv.length + extraBytes + encryptedMessage.length) - .put((byte) iv.length) - .put(iv); + ByteBuffer byteBuffer = ByteBuffer.allocate(1 + ivLength + extraBytes + encryptedMessage.length) + .put((byte) ivLength); + + if(iv != null) { + byteBuffer.put(iv); + } if(includeMessageLength) { byteBuffer.put((byte)(encryptedMessage.length / 256)); @@ -97,7 +102,7 @@ public static SecretKey getSecretKeySpec(String key) throws EncryptionException return new SecretKeySpec(keyBytes, "AES"); } - public static byte[] decrypt(byte[] bytes, Key key, String keyAndTransform, + public static byte[] decrypt(byte[] bytes, Key key, String transform, boolean includeMessageLength) throws EncryptionException { final int TAG_LENGTH_BIT = 128; @@ -128,7 +133,7 @@ public static byte[] decrypt(byte[] bytes, Key key, String keyAndTransform, System.arraycopy(bytes, readIndex, encrypted, 0, encryptedLength); try { - Cipher cipher = Cipher.getInstance(keyAndTransform); + Cipher cipher = Cipher.getInstance(transform); if (includeMessageLength) { cipher.init(Cipher.DECRYPT_MODE, key, iv != null ? new IvParameterSpec(iv) : null); From 59fbfd006959a4a5179d8645f66f62bb5228585c Mon Sep 17 00:00:00 2001 From: David Viggiano <dave.viggiano@gmail.com> Date: Thu, 1 May 2025 03:24:28 -0400 Subject: [PATCH 351/402] Fixed bug (put the readIndex increment where it's supposed to be). --- src/main/java/org/commcare/util/EncryptionUtils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/commcare/util/EncryptionUtils.java b/src/main/java/org/commcare/util/EncryptionUtils.java index dfd9a256d9..5ef5a2179e 100644 --- a/src/main/java/org/commcare/util/EncryptionUtils.java +++ b/src/main/java/org/commcare/util/EncryptionUtils.java @@ -125,6 +125,7 @@ public static byte[] decrypt(byte[] bytes, Key key, String transform, encryptedLength= (bytes[readIndex] & 0xFF) << 8; readIndex++; encryptedLength += (bytes[readIndex] & 0xFF); + readIndex++; } else { encryptedLength = bytes.length - readIndex; } From 5afdbb1bbcc07fe51bdd3a3b254ed1782d348da0 Mon Sep 17 00:00:00 2001 From: David Viggiano <dave.viggiano@gmail.com> Date: Thu, 1 May 2025 07:22:34 -0400 Subject: [PATCH 352/402] Throwing EncryptionException instead of RuntimeException --- src/main/java/org/commcare/util/EncryptionUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/util/EncryptionUtils.java b/src/main/java/org/commcare/util/EncryptionUtils.java index 5ef5a2179e..7b5f913842 100644 --- a/src/main/java/org/commcare/util/EncryptionUtils.java +++ b/src/main/java/org/commcare/util/EncryptionUtils.java @@ -145,7 +145,7 @@ public static byte[] decrypt(byte[] bytes, Key key, String transform, return cipher.doFinal(encrypted); } catch(NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | InvalidAlgorithmParameterException | BadPaddingException | InvalidKeyException e) { - throw new RuntimeException(e); + throw new EncryptionException("Decrypting message failed", e); } } From a9ff2a43564f8edf3fe82b60a733b98772c140ee Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Thu, 1 May 2025 19:12:01 +0530 Subject: [PATCH 353/402] -fixed scroll issue, configure biometric issue, change phone number issue, fixed dimagi logo background --- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 240 +++++++++++++++++++++++ gradlew.bat | 91 +++++++++ 4 files changed, 336 insertions(+) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 LmiCMtn7{r93&V3< literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ae04661ee7 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..a69d9cb6c2 --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..f127cfd49d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From ef08a69b1a9863428f56f6c4674e4875678bea0f Mon Sep 17 00:00:00 2001 From: David Viggiano <dave.viggiano@gmail.com> Date: Thu, 1 May 2025 09:44:58 -0400 Subject: [PATCH 354/402] Removed an obsolete safety check and exception throw. Added a couple javadocs. --- .../org/commcare/util/EncryptionUtils.java | 26 +++++++++++--- .../org/commcare/util/DateRangeUtilsTest.java | 36 +++++++++---------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/commcare/util/EncryptionUtils.java b/src/main/java/org/commcare/util/EncryptionUtils.java index 7b5f913842..4731470494 100644 --- a/src/main/java/org/commcare/util/EncryptionUtils.java +++ b/src/main/java/org/commcare/util/EncryptionUtils.java @@ -19,6 +19,17 @@ public class EncryptionUtils { + /** + * Encrypts a message and produces a base64 encoded payload containing the ciphertext + * The key and transform are specified as inputs to + * A random IV may be generated to encrypt the input (unless using RSA) + * + * @param message a byte[] to be encrypted + * @param key A base64 encoded 256 bit symmetric key + * @param transform The transformation to use for encryption + * @param includeMessageLength Whether to include the message length in the packed payload + * @return A base64 encoded payload containing the IV and AES encrypted ciphertext, which can be decoded by this utility's decrypt method and the same symmetric key + */ public static String encrypt(byte[] message, Key key, String transform, boolean includeMessageLength) throws EncryptionException { final int MIN_IV_LENGTH_BYTE = 1; @@ -102,6 +113,17 @@ public static SecretKey getSecretKeySpec(String key) throws EncryptionException return new SecretKeySpec(keyBytes, "AES"); } + /** + * Decrypts a message and returns the unencrypted byte[] + * The key and transform are specified as inputs + * The IV may be generated to encrypt the input (unless using RSA) + * + * @param bytes a byte[] to be encrypted + * @param key The key that should be used for decryption + * @param transform The transformation to use for decryption + * @param includeMessageLength Whether the message length is included in the packed bytes input + * @return A byte[] containing the unencrypted message + */ public static byte[] decrypt(byte[] bytes, Key key, String transform, boolean includeMessageLength) throws EncryptionException { @@ -109,10 +131,6 @@ public static byte[] decrypt(byte[] bytes, Key key, String transform, int readIndex = 0; int ivLength = bytes[readIndex] & 0xFF; readIndex++; - if (ivLength < 0) { - //Note: Early chance to catch decryption error - throw new EncryptionException("Negative IV length"); - } byte[] iv = null; if (ivLength > 0) { iv = new byte[ivLength]; diff --git a/src/test/java/org/commcare/util/DateRangeUtilsTest.java b/src/test/java/org/commcare/util/DateRangeUtilsTest.java index e46ac253b1..659f394f4a 100644 --- a/src/test/java/org/commcare/util/DateRangeUtilsTest.java +++ b/src/test/java/org/commcare/util/DateRangeUtilsTest.java @@ -1,18 +1,18 @@ -package org.commcare.util; - -import junit.framework.TestCase; - -import org.junit.Test; - -import java.text.ParseException; - -public class DateRangeUtilsTest extends TestCase { - - @Test - public void testDateConversion() throws ParseException { - String dateRange = "2020-02-15 to 2021-03-18"; - String formattedDateRange = DateRangeUtils.formatDateRangeAnswer(dateRange); - assertEquals("__range__2020-02-15__2021-03-18", formattedDateRange); - assertEquals(dateRange, DateRangeUtils.getHumanReadableDateRange(formattedDateRange)); - } -} +//package org.commcare.util; +// +//import junit.framework.TestCase; +// +//import org.junit.Test; +// +//import java.text.ParseException; +// +//public class DateRangeUtilsTest extends TestCase { +// +// @Test +// public void testDateConversion() throws ParseException { +// String dateRange = "2020-02-15 to 2021-03-18"; +// String formattedDateRange = DateRangeUtils.formatDateRangeAnswer(dateRange); +// assertEquals("__range__2020-02-15__2021-03-18", formattedDateRange); +// assertEquals(dateRange, DateRangeUtils.getHumanReadableDateRange(formattedDateRange)); +// } +//} From 2940eb1fb93c2afb8ba0a2713b3a49522c5f8cf7 Mon Sep 17 00:00:00 2001 From: David Viggiano <dave.viggiano@gmail.com> Date: Thu, 1 May 2025 09:46:11 -0400 Subject: [PATCH 355/402] Added back commented out code (accidental commit) --- .../org/commcare/util/DateRangeUtilsTest.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/test/java/org/commcare/util/DateRangeUtilsTest.java b/src/test/java/org/commcare/util/DateRangeUtilsTest.java index 659f394f4a..94ead04caa 100644 --- a/src/test/java/org/commcare/util/DateRangeUtilsTest.java +++ b/src/test/java/org/commcare/util/DateRangeUtilsTest.java @@ -1,18 +1,18 @@ -//package org.commcare.util; -// -//import junit.framework.TestCase; -// -//import org.junit.Test; -// -//import java.text.ParseException; -// -//public class DateRangeUtilsTest extends TestCase { -// -// @Test -// public void testDateConversion() throws ParseException { -// String dateRange = "2020-02-15 to 2021-03-18"; -// String formattedDateRange = DateRangeUtils.formatDateRangeAnswer(dateRange); -// assertEquals("__range__2020-02-15__2021-03-18", formattedDateRange); -// assertEquals(dateRange, DateRangeUtils.getHumanReadableDateRange(formattedDateRange)); -// } -//} +package org.commcare.util; + +import junit.framework.TestCase; + +import org.junit.Test; + +import java.text.ParseException; + +public class DateRangeUtilsTest extends TestCase { + + @Test + public void testDateConversion() throws ParseException { + String dateRange = "2020-02-15 to 2021-03-18"; + String formattedDateRange = DateRangeUtils.formatDateRangeAnswer(dateRange); + assertEquals("__range__2020-02-15__2021-03-18", formattedDateRange); + assertEquals(dateRange, DateRangeUtils.getHumanReadableDateRange(formattedDateRange)); + } +} \ No newline at end of file From a3277784eb8ded0c4769d685c5f1793582c72917 Mon Sep 17 00:00:00 2001 From: David Viggiano <dave.viggiano@gmail.com> Date: Thu, 1 May 2025 09:48:32 -0400 Subject: [PATCH 356/402] Restored missing newline. --- src/test/java/org/commcare/util/DateRangeUtilsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/commcare/util/DateRangeUtilsTest.java b/src/test/java/org/commcare/util/DateRangeUtilsTest.java index 94ead04caa..e46ac253b1 100644 --- a/src/test/java/org/commcare/util/DateRangeUtilsTest.java +++ b/src/test/java/org/commcare/util/DateRangeUtilsTest.java @@ -15,4 +15,4 @@ public void testDateConversion() throws ParseException { assertEquals("__range__2020-02-15__2021-03-18", formattedDateRange); assertEquals(dateRange, DateRangeUtils.getHumanReadableDateRange(formattedDateRange)); } -} \ No newline at end of file +} From a7b37c735d7d14f458e13842d23d2c94f7a1f236 Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Tue, 6 May 2025 19:21:15 +0530 Subject: [PATCH 357/402] -updated the functions for polygon-point and point-inside --- build.gradle | 2 +- .../core/model/utils/PolygonUtils.java | 158 +++++++----------- .../javarosa/xpath/expr/FunctionUtils.java | 4 +- ...va => XPathClosestPointToPolygonFunc.java} | 30 ++-- ...ava => XPathIsPointInsidePolygonFunc.java} | 31 ++-- .../xpath/parser/ast/ASTNodeFunctionCall.java | 8 +- .../javarosa/xpath/test/XPathEvalTest.java | 18 ++ 7 files changed, 111 insertions(+), 140 deletions(-) rename src/main/java/org/javarosa/xpath/expr/{XPathClosestPolygonPointFunc.java => XPathClosestPointToPolygonFunc.java} (65%) rename src/main/java/org/javarosa/xpath/expr/{XPathPointInsidePolygon.java => XPathIsPointInsidePolygonFunc.java} (65%) diff --git a/build.gradle b/build.gradle index 44258c5aab..068bd05cc0 100644 --- a/build.gradle +++ b/build.gradle @@ -83,7 +83,7 @@ dependencies { implementation 'io.opentracing:opentracing-api:0.33.0' implementation 'io.opentracing:opentracing-util:0.33.0' implementation 'com.datadoghq:dd-trace-api:1.10.0' - + implementation 'org.locationtech.jts:jts-core:1.19.0' cliImplementation 'org.json:json:20140107' cliImplementation 'com.github.stefanhaustein:kxml2:2.4.1' cliImplementation 'commons-cli:commons-cli:1.3.1' diff --git a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java index 61f99ed29f..d424ba2a6f 100644 --- a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java @@ -1,124 +1,82 @@ package org.javarosa.core.model.utils; + +import org.javarosa.core.model.data.GeoPointData; +import org.locationtech.jts.geom.*; +import org.locationtech.jts.operation.distance.DistanceOp; +import org.locationtech.jts.operation.valid.IsValidOp; + import java.util.List; +/** + * Utility class for creating, validating, and interacting with geographic polygons. + */ public class PolygonUtils { /** - * Determines if a point is inside a polygon. + * Creates a valid polygon from a list of latitude and longitude strings. * - * @param polygonPoints A list of doubles representing the polygon vertices - * (latitude and longitude pairs). - * @param testPoint A list of doubles representing the latitude and longitude of the test point. - * @return true if the point is inside the polygon, false otherwise. - * This code is written with the help of chatgpt + * @param latLngList List of alternating latitude and longitude strings (e.g., [lat1, lng1, lat2, lng2, ...]) + * @return a valid {@link Polygon} object + * @throws IllegalArgumentException if the input is malformed or the polygon is invalid */ - public static boolean isPointInsidePolygon(List<Double> polygonPoints, double[] testPoint) { - int intersectCount = 0; - int vertexCount = polygonPoints.size() / 2; - - double testLat = testPoint[0]; - double testLng = testPoint[1]; - - for (int i = 0; i < vertexCount; i++) { - double lat1 = polygonPoints.get(2 * i); - double lng1 = polygonPoints.get(2 * i + 1); - double lat2 = polygonPoints.get((2 * ((i + 1) % vertexCount))); - double lng2 = polygonPoints.get((2 * ((i + 1) % vertexCount)) + 1); - - if (rayIntersectsEdge(testLat, testLng, lat1, lng1, lat2, lng2)) { - intersectCount++; - } + public static Polygon createValidatedPolygon(List<String> latLngList) throws IllegalArgumentException { + if (latLngList == null || latLngList.size() < 6 || latLngList.size() % 2 != 0) { + throw new IllegalArgumentException( + "Input must contain at least three lat/lng pairs (six elements total), and must be " + + "even-sized."); } - return (intersectCount % 2 == 1); - } - - /** - * Finds the minimum distance from a point to the polygon and the closest coordinate on the polygon. - * - * @param polygonPoints A list of doubles representing the polygon vertices - * (latitude and longitude pairs). - * @param testPoint A list of doubles representing the latitude and longitude of the test point. - * @return A result containing the minimum distance and the closest point on the polygon. - */ - public static String getClosestPoint(List<Double> polygonPoints, double[] testPoint) { - int numVertices = polygonPoints.size() / 2; - double[] closestPoint = null; - double minDistance = Double.MAX_VALUE; - - for (int i = 0; i < numVertices; i++) { - // Get the start and end points of the current edge - double startX = polygonPoints.get(2 * i); - double startY = polygonPoints.get(2 * i + 1); - double endX = polygonPoints.get(2 * ((i + 1) % numVertices)); - double endY = polygonPoints.get(2 * ((i + 1) % numVertices) + 1); + GeometryFactory geometryFactory = new GeometryFactory(); + int numPoints = latLngList.size() / 2; + Coordinate[] coordinates = new Coordinate[numPoints + 1]; - // Find the closest point on this edge - double[] candidatePoint = getClosestPointOnSegment( - startX, startY, endX, endY, testPoint[0], testPoint[1]); - double distance = distanceBetween(candidatePoint, testPoint); - - // Update the closest point if necessary - if (distance < minDistance) { - minDistance = distance; - closestPoint = candidatePoint; - } + for (int i = 0; i < numPoints; i++) { + double latitude = Double.parseDouble(latLngList.get(i * 2)); + double longitude = Double.parseDouble(latLngList.get(i * 2 + 1)); + coordinates[i] = new Coordinate(longitude, latitude); // JTS uses x=longitude, y=latitude } - // Return the closest point as a space-separated string - return closestPoint[0] + " " + closestPoint[1]; - } + // Close the polygon + coordinates[numPoints] = coordinates[0]; - private static double[] getClosestPointOnSegment(double startX, double startY, double endX, double endY, double px, double py) { - double dx = endX - startX; - double dy = endY - startY; + LinearRing shell = geometryFactory.createLinearRing(coordinates); + Polygon polygon = geometryFactory.createPolygon(shell); - if (dx == 0 && dy == 0) { - // The segment is a single point - return new double[]{startX, startY}; + IsValidOp validator = new IsValidOp(polygon); + if (!validator.isValid()) { + throw new IllegalArgumentException("Invalid polygon: " + validator.getValidationError().getMessage()); } - // Calculate the projection factor t - double t = ((px - startX) * dx + (py - startY) * dy) / (dx * dx + dy * dy); - - // Clamp t to the range [0, 1] to stay on the segment - t = Math.max(0, Math.min(1, t)); - - // Compute the closest point - return new double[]{startX + t * dx, startY + t * dy}; + return polygon; } - private static double distanceBetween(double[] a, double[] b) { - return Math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1])); + /** + * Determines if a given point lies inside or on the boundary of the provided polygon. + * + * @param polygon The {@link Polygon} to test against. + * @param pointData A {@link GeoPointData} representing the test point. + * @return true if the point is inside or on the polygon, false otherwise. + */ + public static boolean isPointInsideOrOnPolygon(Polygon polygon, GeoPointData pointData) { + GeometryFactory geometryFactory = new GeometryFactory(); + Point point = geometryFactory.createPoint( + new Coordinate(pointData.getLongitude(), pointData.getLatitude())); + return polygon.covers(point); } /** - * Checks if a ray starting from the test point intersects the edge defined by two vertices. + * Finds the closest point on the polygon from a given external point. + * + * @param polygon The {@link Polygon} to check against. + * @param pointData A {@link GeoPointData} representing the external point. + * @return A string in the format "lat lng" representing the closest point on the polygon. */ - private static boolean rayIntersectsEdge(double testLat, double testLng, double lat1, double lng1, double lat2, double lng2) { - if (lat1 > lat2) { - double tempLat = lat1, tempLng = lng1; - lat1 = lat2; - lng1 = lng2; - lat2 = tempLat; - lng2 = tempLng; - } - - if (testLat < lat1 || testLat > lat2) { - return false; - } - - if (testLng > Math.max(lng1, lng2)) { - return false; - } - - if (testLng < Math.min(lng1, lng2)) { - return true; - } - - double slope = (lng2 - lng1) / (lat2 - lat1); - double intersectLng = lng1 + (testLat - lat1) * slope; - - return testLng < intersectLng; + public static String getClosestPointOnPolygon(Polygon polygon, GeoPointData pointData) { + GeometryFactory geometryFactory = new GeometryFactory(); + Point externalPoint = geometryFactory.createPoint( + new Coordinate(pointData.getLongitude(), pointData.getLatitude())); + Coordinate[] nearestPoints = DistanceOp.nearestPoints(polygon, externalPoint); + Coordinate closest = nearestPoints[0]; + return closest.y + " " + closest.x; // Return in "lat lng" format } -} +} \ No newline at end of file diff --git a/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java b/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java index 3157fa470d..828ef80f1b 100644 --- a/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java +++ b/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java @@ -90,8 +90,8 @@ public class FunctionUtils { funcList.put(XPathEncryptStringFunc.NAME, XPathEncryptStringFunc.class); funcList.put(XPathDecryptStringFunc.NAME, XPathDecryptStringFunc.class); funcList.put(XPathJsonPropertyFunc.NAME, XPathJsonPropertyFunc.class); - funcList.put(XPathClosestPolygonPointFunc.NAME, XPathClosestPolygonPointFunc.class); - funcList.put(XPathPointInsidePolygon.NAME, XPathPointInsidePolygon.class); + funcList.put(XPathClosestPointToPolygonFunc.NAME, XPathClosestPointToPolygonFunc.class); + funcList.put(XPathIsPointInsidePolygonFunc.NAME, XPathIsPointInsidePolygonFunc.class); } private static final CacheTable<String, Double> mDoubleParseCache = new CacheTable<>(); diff --git a/src/main/java/org/javarosa/xpath/expr/XPathClosestPolygonPointFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java similarity index 65% rename from src/main/java/org/javarosa/xpath/expr/XPathClosestPolygonPointFunc.java rename to src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java index 10b18ef225..60aa0bb348 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathClosestPolygonPointFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java @@ -7,20 +7,20 @@ import org.javarosa.core.model.utils.PolygonUtils; import org.javarosa.xpath.XPathTypeMismatchException; import org.javarosa.xpath.parser.XPathSyntaxException; +import org.locationtech.jts.geom.Polygon; -import java.util.ArrayList; -import java.util.List; +import java.util.Arrays; -public class XPathClosestPolygonPointFunc extends XPathFuncExpr{ +public class XPathClosestPointToPolygonFunc extends XPathFuncExpr { public static final String NAME = "polygon-point"; private static final int EXPECTED_ARG_COUNT = 2; - public XPathClosestPolygonPointFunc() { + public XPathClosestPointToPolygonFunc() { name = NAME; expectedArgCount = EXPECTED_ARG_COUNT; } - public XPathClosestPolygonPointFunc(XPathExpression[] args) throws XPathSyntaxException { + public XPathClosestPointToPolygonFunc(XPathExpression[] args) throws XPathSyntaxException { super(NAME, args, EXPECTED_ARG_COUNT, true); } @@ -32,30 +32,26 @@ public XPathClosestPolygonPointFunc(XPathExpression[] args) throws XPathSyntaxEx */ @Override protected Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) { - return closestPoint(evaluatedArgs[0], evaluatedArgs[1]); + return closestPointToPolygon(evaluatedArgs[0], evaluatedArgs[1]); } - public static String closestPoint(Object from, Object to) { + public static String closestPointToPolygon(Object from, Object to) { String unpackedFrom = (String)FunctionUtils.unpack(from); String unpackedTo = (String)FunctionUtils.unpack(to); if (unpackedFrom == null || "".equals(unpackedFrom) || unpackedTo == null || "".equals(unpackedTo)) { return ""; } try { - String[] coordinates=unpackedFrom.split(" "); - List<Double> polygonList = new ArrayList<Double>(); - - for (String coordinate : coordinates) { - polygonList.add(Double.parseDouble(coordinate)); - } + String[] coordinates = unpackedFrom.split(" "); + Polygon polygon = PolygonUtils.createValidatedPolygon(Arrays.asList(coordinates)); // Casting and uncasting seems strange but is consistent with the codebase - GeoPointData castedTo = new GeoPointData().cast(new UncastData(unpackedTo)); - String closestPointResult=PolygonUtils.getClosestPoint(polygonList,new double[]{castedTo.getLatitude(), castedTo.getLongitude()}); - - return closestPointResult; + GeoPointData pointData = new GeoPointData().cast(new UncastData(unpackedTo)); + return PolygonUtils.getClosestPointOnPolygon(polygon, pointData); } catch (NumberFormatException e) { throw new XPathTypeMismatchException("polygon-point() function requires arguments containing " + "numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo); + } catch (IllegalArgumentException e) { + throw new XPathTypeMismatchException(e.getMessage()); } } } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java similarity index 65% rename from src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java rename to src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java index feb7b8199a..ae26a3dddd 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java @@ -7,54 +7,53 @@ import org.javarosa.core.model.utils.PolygonUtils; import org.javarosa.xpath.XPathTypeMismatchException; import org.javarosa.xpath.parser.XPathSyntaxException; -import java.util.ArrayList; -import java.util.List; +import org.locationtech.jts.geom.Polygon; -public class XPathPointInsidePolygon extends XPathFuncExpr{ +import java.util.Arrays; + +public class XPathIsPointInsidePolygonFunc extends XPathFuncExpr { public static final String NAME = "inside-polygon"; private static final int EXPECTED_ARG_COUNT = 2; + /** * Returns true if the geopoint is inside the polygon, in meters, given objects to unpack. * Ignores altitude and accuracy. * Note that the arguments can be strings. * Returns false if one of the arguments is null or the empty string. */ - public XPathPointInsidePolygon() { + public XPathIsPointInsidePolygonFunc() { name = NAME; expectedArgCount = EXPECTED_ARG_COUNT; } - public XPathPointInsidePolygon(XPathExpression[] args) throws XPathSyntaxException { + public XPathIsPointInsidePolygonFunc(XPathExpression[] args) throws XPathSyntaxException { super(NAME, args, EXPECTED_ARG_COUNT, true); } @Override protected Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) { - return boundaryDistance(evaluatedArgs[0], evaluatedArgs[1]); + return isPointWithinBoundary(evaluatedArgs[0], evaluatedArgs[1]); } - public static boolean boundaryDistance(Object from, Object to) { + public static boolean isPointWithinBoundary(Object from, Object to) { String unpackedFrom = (String)FunctionUtils.unpack(from); String unpackedTo = (String)FunctionUtils.unpack(to); if (unpackedFrom == null || "".equals(unpackedFrom) || unpackedTo == null || "".equals(unpackedTo)) { return false; } try { - String[] coordinates=unpackedFrom.split(" "); - List<Double> polygonList = new ArrayList<Double>(); - - for (String coordinate : coordinates) { - polygonList.add(Double.parseDouble(coordinate)); - } + String[] coordinates = unpackedFrom.split(" "); + Polygon polygon = PolygonUtils.createValidatedPolygon(Arrays.asList(coordinates)); // Casting and uncasting seems strange but is consistent with the codebase - GeoPointData castedTo = new GeoPointData().cast(new UncastData(unpackedTo)); - boolean isInside= PolygonUtils.isPointInsidePolygon(polygonList,new double[]{castedTo.getLatitude(), castedTo.getLongitude()}); + GeoPointData pointData = new GeoPointData().cast(new UncastData(unpackedTo)); - return isInside; + return PolygonUtils.isPointInsideOrOnPolygon(polygon, pointData); } catch (NumberFormatException e) { throw new XPathTypeMismatchException("point-in-boundary() function requires arguments containing " + "numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo); + } catch (IllegalArgumentException e) { + throw new XPathTypeMismatchException(e.getMessage()); } } } \ No newline at end of file diff --git a/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java b/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java index 24b6f23f64..1ad102966f 100755 --- a/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java +++ b/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java @@ -7,7 +7,7 @@ import org.javarosa.xpath.expr.XPathAtanTwoFunc; import org.javarosa.xpath.expr.XPathBooleanFromStringFunc; import org.javarosa.xpath.expr.XPathBooleanFunc; -import org.javarosa.xpath.expr.XPathClosestPolygonPointFunc; +import org.javarosa.xpath.expr.XPathClosestPointToPolygonFunc; import org.javarosa.xpath.expr.XPathCeilingFunc; import org.javarosa.xpath.expr.XPathChecklistFunc; import org.javarosa.xpath.expr.XPathChecksumFunc; @@ -49,7 +49,7 @@ import org.javarosa.xpath.expr.XPathNowFunc; import org.javarosa.xpath.expr.XPathNumberFunc; import org.javarosa.xpath.expr.XPathPiFunc; -import org.javarosa.xpath.expr.XPathPointInsidePolygon; +import org.javarosa.xpath.expr.XPathIsPointInsidePolygonFunc; import org.javarosa.xpath.expr.XPathPositionFunc; import org.javarosa.xpath.expr.XPathPowFunc; import org.javarosa.xpath.expr.XPathQName; @@ -260,9 +260,9 @@ private static XPathFuncExpr buildFuncExpr(String name, XPathExpression[] args) case "json-property": return new XPathJsonPropertyFunc(args); case "polygon-point": - return new XPathClosestPolygonPointFunc(args); + return new XPathClosestPointToPolygonFunc(args); case "inside-polygon": - return new XPathPointInsidePolygon(args); + return new XPathIsPointInsidePolygonFunc(args); default: return new XPathCustomRuntimeFunc(name, args); } diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index ca81349168..857d6d2162 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -610,12 +610,30 @@ public void doTests() { testEval("$var_double_five", null, varContext, Double.valueOf(5.0)); //Polygon point testEval("polygon-point('78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78.041383 27.175569','78.041 27.176')", null, null, "78.041383 27.175569"); // Outside, near bottom-left vertex + testEval("polygon-point('78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78.041383 27.175569','78.041 27.176')", null, null, "78.041383 27.175569"); // Bottom-left vertex + testEval("polygon-point('78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78.041383 27.175569','78.043 27.175')", null, null, "78.04259876 27.175057319999997"); // Near top-right + + testEval("polygon-point('78.041 27.174 78.043 27.174 78.043 27.176 78.041 27.176','78.042 27.175')", null, null, "78.042 27.175"); // Inside polygon + + testEval("polygon-point('78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176','78.042 27.177')", null, null, "78.042 27.176"); // Near top edge + + testEval("polygon-point('78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176','78.039 27.175')", null, null, "78.04 27.175"); // Left of polygon + + testEval("polygon-point('78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176','78.045 27.1755')", null, null, "78.044 27.1755"); // Right side + + testEval("polygon-point('78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176','78.042 27.173')", null, null, "78.042 27.174"); // Bottom side //inside polygon testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.0195 27.204')", null, null, true); // Inside the polygon testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.0205 27.2035')", null, null, false); // Outside the polygon, near bottom-right testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.018 27.204')", null, null, false); // Outside the polygon, far left testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.0187201 27.203509')", null, null, true); // On the polygon vertex + + //Test Polygon + testEval("inside-polygon('0 0 2 2 0 2 2 0','1 1')", null, null, new XPathTypeMismatchException("Invalid polygon: Self-intersection")); // Self-intersecting polygon + testEval("inside-polygon('78.0186987 27.2043773 78.0187201','78.0187201 27.203509')", null, null, new XPathTypeMismatchException("Invalid polygon: Self-intersection")); // Odd count, invalid input + testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509','78.0187201 27.203509')", null, null, new XPathTypeMismatchException("Invalid polygon: Self-intersection") ); // Only 2 points, not a polygon + //Attribute XPath References //testEval("/@blah", null, null, new XPathUnsupportedException()); //TODO: Need to test with model, probably in a different file From 7d08018cf5b57a9a5051ebc1a76cfa83e7a881f2 Mon Sep 17 00:00:00 2001 From: Parth Mittal <parth.mittal@dimagi.com> Date: Tue, 6 May 2025 19:36:45 +0530 Subject: [PATCH 358/402] Delete gradle/wrapper/gradle-wrapper.properties [SKIP CI] --- gradle/wrapper/gradle-wrapper.properties | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 gradle/wrapper/gradle-wrapper.properties diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ae04661ee7..0000000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists From fc8426594169c40ab1dd97c0008a24675634505c Mon Sep 17 00:00:00 2001 From: Parth Mittal <parth.mittal@dimagi.com> Date: Tue, 6 May 2025 19:37:02 +0530 Subject: [PATCH 359/402] Delete gradlew.bat [SKIP CI] --- gradlew.bat | 91 ----------------------------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 gradlew.bat diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index f127cfd49d..0000000000 --- a/gradlew.bat +++ /dev/null @@ -1,91 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega From 68abfee9d126937c75fa0e3f1320b81e122bbe74 Mon Sep 17 00:00:00 2001 From: Parth Mittal <parth.mittal@dimagi.com> Date: Tue, 6 May 2025 19:37:17 +0530 Subject: [PATCH 360/402] Delete gradlew [SKIP CI] --- gradlew | 240 -------------------------------------------------------- 1 file changed, 240 deletions(-) delete mode 100755 gradlew diff --git a/gradlew b/gradlew deleted file mode 100755 index a69d9cb6c2..0000000000 --- a/gradlew +++ /dev/null @@ -1,240 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" From f6472481805656d09e9191da65a0171acf6eac0e Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Tue, 6 May 2025 20:06:08 +0530 Subject: [PATCH 361/402] -renamed functions --- .../core/model/utils/PolygonUtils.java | 4 +- .../expr/XPathClosestPointToPolygonFunc.java | 14 ++-- .../expr/XPathIsPointInsidePolygonFunc.java | 14 ++-- .../javarosa/xpath/test/XPathEvalTest.java | 71 +++++++++++++------ 4 files changed, 66 insertions(+), 37 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java index d424ba2a6f..5eed8df9d2 100644 --- a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java @@ -57,7 +57,7 @@ public static Polygon createValidatedPolygon(List<String> latLngList) throws Ill * @param pointData A {@link GeoPointData} representing the test point. * @return true if the point is inside or on the polygon, false otherwise. */ - public static boolean isPointInsideOrOnPolygon(Polygon polygon, GeoPointData pointData) { + public static boolean isPointInsideOrOnPolygon(GeoPointData pointData,Polygon polygon) { GeometryFactory geometryFactory = new GeometryFactory(); Point point = geometryFactory.createPoint( new Coordinate(pointData.getLongitude(), pointData.getLatitude())); @@ -71,7 +71,7 @@ public static boolean isPointInsideOrOnPolygon(Polygon polygon, GeoPointData poi * @param pointData A {@link GeoPointData} representing the external point. * @return A string in the format "lat lng" representing the closest point on the polygon. */ - public static String getClosestPointOnPolygon(Polygon polygon, GeoPointData pointData) { + public static String getClosestPointOnPolygon(GeoPointData pointData,Polygon polygon) { GeometryFactory geometryFactory = new GeometryFactory(); Point externalPoint = geometryFactory.createPoint( new Coordinate(pointData.getLongitude(), pointData.getLatitude())); diff --git a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java index 60aa0bb348..2159907ab9 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java @@ -36,20 +36,20 @@ protected Object evalBody(DataInstance model, EvaluationContext evalContext, Obj } public static String closestPointToPolygon(Object from, Object to) { - String unpackedFrom = (String)FunctionUtils.unpack(from); - String unpackedTo = (String)FunctionUtils.unpack(to); - if (unpackedFrom == null || "".equals(unpackedFrom) || unpackedTo == null || "".equals(unpackedTo)) { + String inputPoint = (String)FunctionUtils.unpack(from); + String inputPolygon = (String)FunctionUtils.unpack(to); + if (inputPoint == null || "".equals(inputPoint) || inputPolygon == null || "".equals(inputPolygon)) { return ""; } try { - String[] coordinates = unpackedFrom.split(" "); + String[] coordinates = inputPolygon.split(" "); Polygon polygon = PolygonUtils.createValidatedPolygon(Arrays.asList(coordinates)); // Casting and uncasting seems strange but is consistent with the codebase - GeoPointData pointData = new GeoPointData().cast(new UncastData(unpackedTo)); - return PolygonUtils.getClosestPointOnPolygon(polygon, pointData); + GeoPointData pointData = new GeoPointData().cast(new UncastData(inputPoint)); + return PolygonUtils.getClosestPointOnPolygon(pointData, polygon); } catch (NumberFormatException e) { throw new XPathTypeMismatchException("polygon-point() function requires arguments containing " + - "numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo); + "numeric values only, but received arguments: " + inputPoint + " and " + inputPolygon); } catch (IllegalArgumentException e) { throw new XPathTypeMismatchException(e.getMessage()); } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java index ae26a3dddd..c977665117 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java @@ -37,21 +37,21 @@ protected Object evalBody(DataInstance model, EvaluationContext evalContext, Obj } public static boolean isPointWithinBoundary(Object from, Object to) { - String unpackedFrom = (String)FunctionUtils.unpack(from); - String unpackedTo = (String)FunctionUtils.unpack(to); - if (unpackedFrom == null || "".equals(unpackedFrom) || unpackedTo == null || "".equals(unpackedTo)) { + String inputPoint = (String)FunctionUtils.unpack(from); + String inputPolygon = (String)FunctionUtils.unpack(to); + if (inputPoint == null || "".equals(inputPoint) || inputPolygon == null || "".equals(inputPolygon)) { return false; } try { - String[] coordinates = unpackedFrom.split(" "); + String[] coordinates = inputPolygon.split(" "); Polygon polygon = PolygonUtils.createValidatedPolygon(Arrays.asList(coordinates)); // Casting and uncasting seems strange but is consistent with the codebase - GeoPointData pointData = new GeoPointData().cast(new UncastData(unpackedTo)); + GeoPointData pointData = new GeoPointData().cast(new UncastData(inputPoint)); - return PolygonUtils.isPointInsideOrOnPolygon(polygon, pointData); + return PolygonUtils.isPointInsideOrOnPolygon(pointData, polygon); } catch (NumberFormatException e) { throw new XPathTypeMismatchException("point-in-boundary() function requires arguments containing " + - "numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo); + "numeric values only, but received arguments: " + inputPoint + " and " + inputPolygon); } catch (IllegalArgumentException e) { throw new XPathTypeMismatchException(e.getMessage()); } diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index 857d6d2162..b597ac3632 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -609,30 +609,59 @@ public void doTests() { testEval("$var_int_five", null, varContext, Double.valueOf(5.0)); testEval("$var_double_five", null, varContext, Double.valueOf(5.0)); //Polygon point - testEval("polygon-point('78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78.041383 27.175569','78.041 27.176')", null, null, "78.041383 27.175569"); // Outside, near bottom-left vertex - testEval("polygon-point('78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78.041383 27.175569','78.041 27.176')", null, null, "78.041383 27.175569"); // Bottom-left vertex - - testEval("polygon-point('78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78.041383 27.175569','78.043 27.175')", null, null, "78.04259876 27.175057319999997"); // Near top-right - - testEval("polygon-point('78.041 27.174 78.043 27.174 78.043 27.176 78.041 27.176','78.042 27.175')", null, null, "78.042 27.175"); // Inside polygon - - testEval("polygon-point('78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176','78.042 27.177')", null, null, "78.042 27.176"); // Near top edge - - testEval("polygon-point('78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176','78.039 27.175')", null, null, "78.04 27.175"); // Left of polygon - - testEval("polygon-point('78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176','78.045 27.1755')", null, null, "78.044 27.1755"); // Right side - - testEval("polygon-point('78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176','78.042 27.173')", null, null, "78.042 27.174"); // Bottom side + testEval( + "polygon-point('78.041 27.176','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78" + + ".041383 27.175569')", + null, null, "78.041383 27.175569"); // Outside, near bottom-left vertex + testEval( + "polygon-point('78.041 27.176','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78" + + ".041383 27.175569')", + null, null, "78.041383 27.175569"); // Bottom-left vertex + + testEval( + "polygon-point('78.043 27.175','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78" + + ".041383 27.175569')", + null, null, "78.04259876 27.175057319999997"); // Near top-right + + testEval("polygon-point('78.042 27.175','78.041 27.174 78.043 27.174 78.043 27.176 78.041 27.176')", null, + null, "78.042 27.175"); // Inside polygon + + testEval("polygon-point('78.042 27.177','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, + null, "78.042 27.176"); // Near top edge + + testEval("polygon-point('78.039 27.175','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, + null, "78.04 27.175"); // Left of polygon + + testEval("polygon-point('78.045 27.1755','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, + null, "78.044 27.1755"); // Right side + + testEval("polygon-point('78.042 27.173','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, + null, "78.042 27.174"); // Bottom side //inside polygon - testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.0195 27.204')", null, null, true); // Inside the polygon - testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.0205 27.2035')", null, null, false); // Outside the polygon, near bottom-right - testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.018 27.204')", null, null, false); // Outside the polygon, far left - testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.0187201 27.203509')", null, null, true); // On the polygon vertex + testEval( + "inside-polygon('78.0195 27.204','78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27" + + ".2035281 78.0203027 27.2044155')", + null, null, true); // Inside the polygon + testEval( + "inside-polygon('78.0205 27.2035','78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27" + + ".2035281 78.0203027 27.2044155')", + null, null, false); // Outside the polygon, near bottom-right + testEval( + "inside-polygon('78.018 27.204', '78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27" + + ".2035281 78.0203027 27.2044155')", + null, null, false); // Outside the polygon, far left + testEval( + "inside-polygon('78.0187201 27.203509', '78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155')", + null, null, true); // On the polygon vertex //Test Polygon - testEval("inside-polygon('0 0 2 2 0 2 2 0','1 1')", null, null, new XPathTypeMismatchException("Invalid polygon: Self-intersection")); // Self-intersecting polygon - testEval("inside-polygon('78.0186987 27.2043773 78.0187201','78.0187201 27.203509')", null, null, new XPathTypeMismatchException("Invalid polygon: Self-intersection")); // Odd count, invalid input - testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509','78.0187201 27.203509')", null, null, new XPathTypeMismatchException("Invalid polygon: Self-intersection") ); // Only 2 points, not a polygon + testEval("inside-polygon('1 1','0 0 2 2 0 2 2 0')", null, null, new XPathTypeMismatchException( + "Invalid polygon: Self-intersection")); // Self-intersecting polygon + testEval("inside-polygon('78.0187201 27.203509','78.0186987 27.2043773 78.0187201')", null, null, + new XPathTypeMismatchException("Invalid polygon: Self-intersection")); // Odd count, invalid input + testEval("inside-polygon('78.0187201 27.203509','78.0186987 27.2043773 78.0187201 27.203509')", null, null, + new XPathTypeMismatchException( + "Invalid polygon: Self-intersection")); // Only 2 points, not a polygon //Attribute XPath References //testEval("/@blah", null, null, new XPathUnsupportedException()); From 65912042470637b2a02f2275a62239f473a1ea12 Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Tue, 6 May 2025 20:08:28 +0530 Subject: [PATCH 362/402] -removing the file as it was mistakenly pushed [SKIP CI] --- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 249e5832f090a2944b7473328c07c9755baa3196..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60756 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 LmiCMtn7{r93&V3< From a15919fdf1d84ffd264e4d15e8a65a512178a194 Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Wed, 7 May 2025 15:37:28 +0530 Subject: [PATCH 363/402] -changed the name of the functions, updated the java doc , and exception handling --- .../core/model/utils/PolygonUtils.java | 2 +- .../expr/XPathClosestPointToPolygonFunc.java | 40 ++++++++++++--- .../expr/XPathIsPointInsidePolygonFunc.java | 49 +++++++++++++------ .../xpath/parser/ast/ASTNodeFunctionCall.java | 4 +- .../javarosa/xpath/test/XPathEvalTest.java | 34 ++++++------- 5 files changed, 87 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java index 5eed8df9d2..6f36358ecb 100644 --- a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java @@ -19,7 +19,7 @@ public class PolygonUtils { * @return a valid {@link Polygon} object * @throws IllegalArgumentException if the input is malformed or the polygon is invalid */ - public static Polygon createValidatedPolygon(List<String> latLngList) throws IllegalArgumentException { + public static Polygon createPolygon(List<String> latLngList) throws IllegalArgumentException { if (latLngList == null || latLngList.size() < 6 || latLngList.size() % 2 != 0) { throw new IllegalArgumentException( "Input must contain at least three lat/lng pairs (six elements total), and must be " diff --git a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java index 2159907ab9..3bbe5f5d52 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java @@ -5,14 +5,41 @@ import org.javarosa.core.model.data.UncastData; import org.javarosa.core.model.instance.DataInstance; import org.javarosa.core.model.utils.PolygonUtils; +import org.javarosa.xpath.XPathException; import org.javarosa.xpath.XPathTypeMismatchException; import org.javarosa.xpath.parser.XPathSyntaxException; import org.locationtech.jts.geom.Polygon; import java.util.Arrays; +/** + * XPath function "closest-point-on-polygon()" computes the closest point on the boundary of a polygon + * to a given geographic point. + * + * <p><strong>Syntax:</strong></p> + * <pre> + * closest-point-on-polygon(point_coord,polygon_cord) + * </pre> + * + * <p><strong>Parameters:</strong></p> + * <ul> + * <li><code>polygon_coords</code>: A space-separated string of lon/lat pairs (e.g. "'78.041309 27.174957 78 + * .042574 27.174884 78.042661 27.175493 78.041383 27.175569'")</li> + * <li><code>point_coord</code>: A single point as "lon lat eg('78.043 27.175)"</li> + * </ul> + * + * <p><strong>Returns:</strong></p> + * <p>The closest point on the polygon's boundary to the input point, in "lat lon" format. If the input is + * invalid, IllegalArgumentException.</p> + * + * <p><strong>Recommended Use:</strong></p> + * <pre> + * closest-point-on-polygon('78.041 27.176','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78.041383 27.175569') + * </pre> + * <p>This example finds the closest point on the polygon to (78.041, 27.176)</p> + */ public class XPathClosestPointToPolygonFunc extends XPathFuncExpr { - public static final String NAME = "polygon-point"; + public static final String NAME = "closest-point-on-polygon"; private static final int EXPECTED_ARG_COUNT = 2; public XPathClosestPointToPolygonFunc() { @@ -35,23 +62,22 @@ protected Object evalBody(DataInstance model, EvaluationContext evalContext, Obj return closestPointToPolygon(evaluatedArgs[0], evaluatedArgs[1]); } - public static String closestPointToPolygon(Object from, Object to) { + private static String closestPointToPolygon(Object from, Object to) { String inputPoint = (String)FunctionUtils.unpack(from); String inputPolygon = (String)FunctionUtils.unpack(to); if (inputPoint == null || "".equals(inputPoint) || inputPolygon == null || "".equals(inputPolygon)) { - return ""; + throw new XPathException("closest-point-on-polygon() function requires coordinates of point and polygon"); } try { String[] coordinates = inputPolygon.split(" "); - Polygon polygon = PolygonUtils.createValidatedPolygon(Arrays.asList(coordinates)); - // Casting and uncasting seems strange but is consistent with the codebase + Polygon polygon = PolygonUtils.createPolygon(Arrays.asList(coordinates)); GeoPointData pointData = new GeoPointData().cast(new UncastData(inputPoint)); return PolygonUtils.getClosestPointOnPolygon(pointData, polygon); } catch (NumberFormatException e) { - throw new XPathTypeMismatchException("polygon-point() function requires arguments containing " + + throw new XPathTypeMismatchException("closest-point-on-polygon() function requires arguments containing " + "numeric values only, but received arguments: " + inputPoint + " and " + inputPolygon); } catch (IllegalArgumentException e) { - throw new XPathTypeMismatchException(e.getMessage()); + throw new XPathException(e.getMessage()); } } } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java index c977665117..4e4248ea52 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java @@ -5,22 +5,43 @@ import org.javarosa.core.model.data.UncastData; import org.javarosa.core.model.instance.DataInstance; import org.javarosa.core.model.utils.PolygonUtils; +import org.javarosa.xpath.XPathException; import org.javarosa.xpath.XPathTypeMismatchException; import org.javarosa.xpath.parser.XPathSyntaxException; import org.locationtech.jts.geom.Polygon; import java.util.Arrays; +/** + * XPath function "is-point-inside-polygon()" determines whether a geographic point lies inside + * or on the boundary of a polygon. + * + * <p><strong>Syntax:</strong></p> + * <pre> + * is-point-inside-polygon(point_coord, polygon_coords) + * </pre> + * + * <p><strong>Parameters:</strong></p> + * <ul> + * <li><code>polygon_coords</code>: A space-separated string of lon/lat pairs (e.g. "lon1 lat1 lon2 lat2 ...")</li> + * <li><code>point_coord</code>: A single point as "lon lat"</li> + * </ul> + * + * <p><strong>Returns:</strong></p> + * <p><code>true</code> if the point is strictly inside or on the edge/vertex of the polygon, + * <code>false</code> otherwise, or if inputs are invalid.</p> + * + * <p><strong>Recommended Use:</strong></p> + * <pre> + * is-point-inside-polygon('78.041309 27.174957','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78.041383 27.175569') + * </pre> + * <p>This example checks whether the point (78.041309, 27.174957) lies inside or on the polygon boundary. + * It will return <code>true</code> because the point is exactly on one of the polygon's vertices.</p> + */ public class XPathIsPointInsidePolygonFunc extends XPathFuncExpr { - public static final String NAME = "inside-polygon"; + public static final String NAME = "is-point-inside-polygon"; private static final int EXPECTED_ARG_COUNT = 2; - /** - * Returns true if the geopoint is inside the polygon, in meters, given objects to unpack. - * Ignores altitude and accuracy. - * Note that the arguments can be strings. - * Returns false if one of the arguments is null or the empty string. - */ public XPathIsPointInsidePolygonFunc() { name = NAME; expectedArgCount = EXPECTED_ARG_COUNT; @@ -30,30 +51,28 @@ public XPathIsPointInsidePolygonFunc(XPathExpression[] args) throws XPathSyntaxE super(NAME, args, EXPECTED_ARG_COUNT, true); } - @Override protected Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) { return isPointWithinBoundary(evaluatedArgs[0], evaluatedArgs[1]); } - public static boolean isPointWithinBoundary(Object from, Object to) { + private static boolean isPointWithinBoundary(Object from, Object to) { String inputPoint = (String)FunctionUtils.unpack(from); String inputPolygon = (String)FunctionUtils.unpack(to); if (inputPoint == null || "".equals(inputPoint) || inputPolygon == null || "".equals(inputPolygon)) { - return false; + throw new XPathException("is-point-inside-polygon() function requires coordinates of point and polygon"); } try { String[] coordinates = inputPolygon.split(" "); - Polygon polygon = PolygonUtils.createValidatedPolygon(Arrays.asList(coordinates)); - // Casting and uncasting seems strange but is consistent with the codebase + Polygon polygon = PolygonUtils.createPolygon(Arrays.asList(coordinates)); GeoPointData pointData = new GeoPointData().cast(new UncastData(inputPoint)); return PolygonUtils.isPointInsideOrOnPolygon(pointData, polygon); } catch (NumberFormatException e) { - throw new XPathTypeMismatchException("point-in-boundary() function requires arguments containing " + + throw new XPathTypeMismatchException("is-point-inside-polygon() function requires arguments containing " + "numeric values only, but received arguments: " + inputPoint + " and " + inputPolygon); } catch (IllegalArgumentException e) { - throw new XPathTypeMismatchException(e.getMessage()); + throw new XPathException(e.getMessage()); } } -} \ No newline at end of file +} diff --git a/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java b/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java index 1ad102966f..39357faa0a 100755 --- a/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java +++ b/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java @@ -259,9 +259,9 @@ private static XPathFuncExpr buildFuncExpr(String name, XPathExpression[] args) return new XPathDecryptStringFunc(args); case "json-property": return new XPathJsonPropertyFunc(args); - case "polygon-point": + case "closest-point-on-polygon": return new XPathClosestPointToPolygonFunc(args); - case "inside-polygon": + case "is-point-inside-polygon": return new XPathIsPointInsidePolygonFunc(args); default: return new XPathCustomRuntimeFunc(name, args); diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index b597ac3632..9fba88c124 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -610,57 +610,57 @@ public void doTests() { testEval("$var_double_five", null, varContext, Double.valueOf(5.0)); //Polygon point testEval( - "polygon-point('78.041 27.176','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78" + "closest-point-on-polygon('78.041 27.176','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78" + ".041383 27.175569')", null, null, "78.041383 27.175569"); // Outside, near bottom-left vertex testEval( - "polygon-point('78.041 27.176','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78" + "closest-point-on-polygon('78.041 27.176','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78" + ".041383 27.175569')", null, null, "78.041383 27.175569"); // Bottom-left vertex testEval( - "polygon-point('78.043 27.175','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78" + "closest-point-on-polygon('78.043 27.175','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78" + ".041383 27.175569')", null, null, "78.04259876 27.175057319999997"); // Near top-right - testEval("polygon-point('78.042 27.175','78.041 27.174 78.043 27.174 78.043 27.176 78.041 27.176')", null, + testEval("closest-point-on-polygon('78.042 27.175','78.041 27.174 78.043 27.174 78.043 27.176 78.041 27.176')", null, null, "78.042 27.175"); // Inside polygon - testEval("polygon-point('78.042 27.177','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, + testEval("closest-point-on-polygon('78.042 27.177','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, null, "78.042 27.176"); // Near top edge - testEval("polygon-point('78.039 27.175','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, + testEval("closest-point-on-polygon('78.039 27.175','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, null, "78.04 27.175"); // Left of polygon - testEval("polygon-point('78.045 27.1755','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, + testEval("closest-point-on-polygon('78.045 27.1755','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, null, "78.044 27.1755"); // Right side - testEval("polygon-point('78.042 27.173','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, + testEval("closest-point-on-polygon('78.042 27.173','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, null, "78.042 27.174"); // Bottom side //inside polygon testEval( - "inside-polygon('78.0195 27.204','78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27" + "is-point-inside-polygon('78.0195 27.204','78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27" + ".2035281 78.0203027 27.2044155')", null, null, true); // Inside the polygon testEval( - "inside-polygon('78.0205 27.2035','78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27" + "is-point-inside-polygon('78.0205 27.2035','78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27" + ".2035281 78.0203027 27.2044155')", null, null, false); // Outside the polygon, near bottom-right testEval( - "inside-polygon('78.018 27.204', '78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27" + "is-point-inside-polygon('78.018 27.204', '78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27" + ".2035281 78.0203027 27.2044155')", null, null, false); // Outside the polygon, far left testEval( - "inside-polygon('78.0187201 27.203509', '78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155')", + "is-point-inside-polygon('78.0187201 27.203509', '78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155')", null, null, true); // On the polygon vertex //Test Polygon - testEval("inside-polygon('1 1','0 0 2 2 0 2 2 0')", null, null, new XPathTypeMismatchException( + testEval("is-point-inside-polygon('1 1','0 0 2 2 0 2 2 0')", null, null, new XPathException( "Invalid polygon: Self-intersection")); // Self-intersecting polygon - testEval("inside-polygon('78.0187201 27.203509','78.0186987 27.2043773 78.0187201')", null, null, - new XPathTypeMismatchException("Invalid polygon: Self-intersection")); // Odd count, invalid input - testEval("inside-polygon('78.0187201 27.203509','78.0186987 27.2043773 78.0187201 27.203509')", null, null, - new XPathTypeMismatchException( + testEval("is-point-inside-polygon('78.0187201 27.203509','78.0186987 27.2043773 78.0187201')", null, null, + new XPathException("Invalid polygon: Self-intersection")); // Odd count, invalid input + testEval("is-point-inside-polygon('78.0187201 27.203509','78.0186987 27.2043773 78.0187201 27.203509')", null, null, + new XPathException( "Invalid polygon: Self-intersection")); // Only 2 points, not a polygon //Attribute XPath References From 7d4a01b67af6c7ef156a40a077f737fcb7eaf6b3 Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Wed, 7 May 2025 16:55:32 +0530 Subject: [PATCH 364/402] -Changed Function name --- src/main/java/org/javarosa/xpath/expr/FunctionUtils.java | 2 +- ...PolygonFunc.java => XPathClosestPointOnPolygonFunc.java} | 6 +++--- .../org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/main/java/org/javarosa/xpath/expr/{XPathClosestPointToPolygonFunc.java => XPathClosestPointOnPolygonFunc.java} (95%) diff --git a/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java b/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java index 828ef80f1b..ebfee3f6c0 100644 --- a/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java +++ b/src/main/java/org/javarosa/xpath/expr/FunctionUtils.java @@ -90,7 +90,7 @@ public class FunctionUtils { funcList.put(XPathEncryptStringFunc.NAME, XPathEncryptStringFunc.class); funcList.put(XPathDecryptStringFunc.NAME, XPathDecryptStringFunc.class); funcList.put(XPathJsonPropertyFunc.NAME, XPathJsonPropertyFunc.class); - funcList.put(XPathClosestPointToPolygonFunc.NAME, XPathClosestPointToPolygonFunc.class); + funcList.put(XPathClosestPointOnPolygonFunc.NAME, XPathClosestPointOnPolygonFunc.class); funcList.put(XPathIsPointInsidePolygonFunc.NAME, XPathIsPointInsidePolygonFunc.class); } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java similarity index 95% rename from src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java rename to src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java index 3bbe5f5d52..a2e49fb458 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointToPolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java @@ -38,16 +38,16 @@ * </pre> * <p>This example finds the closest point on the polygon to (78.041, 27.176)</p> */ -public class XPathClosestPointToPolygonFunc extends XPathFuncExpr { +public class XPathClosestPointOnPolygonFunc extends XPathFuncExpr { public static final String NAME = "closest-point-on-polygon"; private static final int EXPECTED_ARG_COUNT = 2; - public XPathClosestPointToPolygonFunc() { + public XPathClosestPointOnPolygonFunc() { name = NAME; expectedArgCount = EXPECTED_ARG_COUNT; } - public XPathClosestPointToPolygonFunc(XPathExpression[] args) throws XPathSyntaxException { + public XPathClosestPointOnPolygonFunc(XPathExpression[] args) throws XPathSyntaxException { super(NAME, args, EXPECTED_ARG_COUNT, true); } diff --git a/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java b/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java index 39357faa0a..f9bef32930 100755 --- a/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java +++ b/src/main/java/org/javarosa/xpath/parser/ast/ASTNodeFunctionCall.java @@ -7,7 +7,7 @@ import org.javarosa.xpath.expr.XPathAtanTwoFunc; import org.javarosa.xpath.expr.XPathBooleanFromStringFunc; import org.javarosa.xpath.expr.XPathBooleanFunc; -import org.javarosa.xpath.expr.XPathClosestPointToPolygonFunc; +import org.javarosa.xpath.expr.XPathClosestPointOnPolygonFunc; import org.javarosa.xpath.expr.XPathCeilingFunc; import org.javarosa.xpath.expr.XPathChecklistFunc; import org.javarosa.xpath.expr.XPathChecksumFunc; @@ -260,7 +260,7 @@ private static XPathFuncExpr buildFuncExpr(String name, XPathExpression[] args) case "json-property": return new XPathJsonPropertyFunc(args); case "closest-point-on-polygon": - return new XPathClosestPointToPolygonFunc(args); + return new XPathClosestPointOnPolygonFunc(args); case "is-point-inside-polygon": return new XPathIsPointInsidePolygonFunc(args); default: From 68a828cd10d10362f41b0e2f1cfd9222a1f7bbea Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Fri, 9 May 2025 17:53:31 +0530 Subject: [PATCH 365/402] remove db lock from async entity to fix a deadlock On calling evaluate on Xpath expression involving case db, the general order of locking was - db lock xpath cache lock implicit db lock caused by db.write When another thread calls eval on another Xpath expression with instance('casedb'), the locking order is - xpath cache lock implicit db lock caused by db.write This causes a deadlock as the AsyncEntity first gets the db lock and then xpath cache lock but the other threads do it in reverse order. --- .../commcare/cases/entity/AsyncEntity.java | 107 +++++++----------- 1 file changed, 42 insertions(+), 65 deletions(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncEntity.java b/src/main/java/org/commcare/cases/entity/AsyncEntity.java index 0e71ee1ea5..0e4667176b 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncEntity.java +++ b/src/main/java/org/commcare/cases/entity/AsyncEntity.java @@ -17,8 +17,6 @@ import org.javarosa.xpath.expr.XPathExpression; import org.javarosa.xpath.parser.XPathSyntaxException; -import java.io.Closeable; -import java.io.IOException; import java.util.Enumeration; import java.util.Hashtable; @@ -59,15 +57,7 @@ public class AsyncEntity extends Entity<TreeReference> { private final EntityStorageCache mEntityStorageCache; /* - * the Object's lock. NOTE: _DO NOT LOCK ANY CODE WHICH READS/WRITES THE CACHE - * UNTIL YOU HAVE A LOCK FOR THE DB! - * - * The lock is for the integrity of this object, not the larger environment, - * and any DB access has its own implict lock between threads, so it's easy - * to accidentally deadlock if you don't already have the db lock - * - * Basically you should never be calling mEntityStorageCache from inside of - * a lock that + * the Object's lock for the integrity of this object */ private final Object mAsyncLock = new Object(); @@ -118,32 +108,25 @@ public Object getField(int i) { data[i] = evaluateField(i); return data[i]; } - } - try (Closeable ignored = mEntityStorageCache != null ? mEntityStorageCache.lockCache() : null) { - synchronized (mAsyncLock) { - if (data[i] == null) { - String cacheKey = null; - if (mEntityStorageCache != null && mCacheIndex != null) { - cacheKey = mEntityStorageCache.getCacheKey(mDetailId, String.valueOf(i), TYPE_NORMAL_FIELD); - // Return from the cache if we have a value - String value = mEntityStorageCache.retrieveCacheValue(mCacheIndex, cacheKey); - if (value != null) { - data[i] = value; - return data[i]; - } - } - // Otherwise evaluate, cache and return the value - data[i] = evaluateField(i); - if (mEntityStorageCache != null && mCacheIndex != null) { - mEntityStorageCache.cache(mCacheIndex, cacheKey, String.valueOf(data[i])); + String cacheKey = null; + if (data[i] == null) { + if (mEntityStorageCache != null && mCacheIndex != null) { + cacheKey = mEntityStorageCache.getCacheKey(mDetailId, String.valueOf(i), + TYPE_NORMAL_FIELD); + // Return from the cache if we have a value + String value = mEntityStorageCache.retrieveCacheValue(mCacheIndex, cacheKey); + if (value != null) { + data[i] = value; + return data[i]; } } - return data[i]; } - } catch (IOException e) { - Logger.exception("Error while getting field", e); + data[i] = evaluateField(i); + if (mEntityStorageCache != null && mCacheIndex != null) { + mEntityStorageCache.cache(mCacheIndex, cacheKey, String.valueOf(data[i])); + } } - return null; + return data[i]; } private Object evaluateField(int i) { @@ -178,43 +161,37 @@ public String getSortField(int i) { evaluateSortData(i); return sortData[i]; } - } - try (Closeable ignored = mEntityStorageCache != null ? mEntityStorageCache.lockCache() : null) { - //get our second lock. - synchronized (mAsyncLock) { - if (sortData[i] == null) { - Text sortText = fields[i].getSort(); - if (sortText == null) { - return null; + + String cacheKey = null; + if (sortData[i] == null) { + Text sortText = fields[i].getSort(); + if (sortText == null) { + return null; + } + + if (mEntityStorageCache != null) { + if (cacheEnabled) { + cacheKey = mEntityStorageCache.getCacheKey(mDetailId, String.valueOf(i), + TYPE_SORT_FIELD); + } else { + // old cache and index + cacheKey = i + "_" + TYPE_SORT_FIELD; } - String cacheKey; - if (mEntityStorageCache != null) { - if (cacheEnabled) { - cacheKey = mEntityStorageCache.getCacheKey(mDetailId, String.valueOf(i), - TYPE_SORT_FIELD); - } else { - // old cache and index - cacheKey = i + "_" + TYPE_SORT_FIELD; - } - if (mCacheIndex != null) { - //Check the cache! - String value = mEntityStorageCache.retrieveCacheValue(mCacheIndex, cacheKey); - if (value != null) { - this.setSortData(i, value); - return sortData[i]; - } - // sort data not in search field cache; load and store it - evaluateSortData(i); - mEntityStorageCache.cache(mCacheIndex, cacheKey, sortData[i]); + if (mCacheIndex != null) { + //Check the cache! + String value = mEntityStorageCache.retrieveCacheValue(mCacheIndex, cacheKey); + if (value != null) { + this.setSortData(i, value); + return sortData[i]; } + } } - return sortData[i]; } - } catch (IOException e) { - Logger.exception("Error while getting sort field", e); + evaluateSortData(i); + mEntityStorageCache.cache(mCacheIndex, cacheKey, sortData[i]); + return sortData[i]; } - return null; } private void evaluateSortData(int i) { @@ -331,7 +308,7 @@ private static String[] breakUpField(String input) { @Override public String getGroupKey() { if (mDetailGroup != null) { - return (String)mDetailGroup.getFunction().eval(context); + return (String)mDetailGroup.getFunction().eval(context); } return null; } From e8a574e063cdceeacf6cf72c1d6b0582310ffc3e Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Fri, 9 May 2025 19:19:45 +0530 Subject: [PATCH 366/402] only cache when cache is defined --- src/main/java/org/commcare/cases/entity/AsyncEntity.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/cases/entity/AsyncEntity.java b/src/main/java/org/commcare/cases/entity/AsyncEntity.java index 0e4667176b..43d5f33290 100755 --- a/src/main/java/org/commcare/cases/entity/AsyncEntity.java +++ b/src/main/java/org/commcare/cases/entity/AsyncEntity.java @@ -189,7 +189,9 @@ public String getSortField(int i) { } } evaluateSortData(i); - mEntityStorageCache.cache(mCacheIndex, cacheKey, sortData[i]); + if (mEntityStorageCache != null && mCacheIndex != null) { + mEntityStorageCache.cache(mCacheIndex, cacheKey, sortData[i]); + } return sortData[i]; } } From f2d9bb19916149df6d8ce0e625abe7a66efd3aaf Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Thu, 12 Jun 2025 20:32:55 +0530 Subject: [PATCH 367/402] -added test cases and convert the input format from lng lat to lat lng --- .../expr/XPathClosestPointOnPolygonFunc.java | 10 +- .../expr/XPathIsPointInsidePolygonFunc.java | 8 +- .../javarosa/xpath/test/XPathEvalTest.java | 103 +++++++++++------- 3 files changed, 73 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java index a2e49fb458..f2a8865966 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java @@ -23,9 +23,9 @@ * * <p><strong>Parameters:</strong></p> * <ul> - * <li><code>polygon_coords</code>: A space-separated string of lon/lat pairs (e.g. "'78.041309 27.174957 78 - * .042574 27.174884 78.042661 27.175493 78.041383 27.175569'")</li> - * <li><code>point_coord</code>: A single point as "lon lat eg('78.043 27.175)"</li> + * <li><code>polygon_coords</code>: A space-separated string of lat/lon pairs (e.g. "'27.174957 78.041309 27 + * .174884 78.042574 27.175493 78.042661 27.175569 78.041383'")</li> + * <li><code>point_coord</code>: A single point as "lat lon eg('27.175 78.043')"</li> * </ul> * * <p><strong>Returns:</strong></p> @@ -34,9 +34,9 @@ * * <p><strong>Recommended Use:</strong></p> * <pre> - * closest-point-on-polygon('78.041 27.176','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78.041383 27.175569') + * closest-point-on-polygon('27.176 78.041','27.174957 78.041309 27.174884 78.042574 27.175493 78.042661 27.175569 78.041383') * </pre> - * <p>This example finds the closest point on the polygon to (78.041, 27.176)</p> + * <p>This example finds the closest point on the polygon to (27.176 78.041)</p> */ public class XPathClosestPointOnPolygonFunc extends XPathFuncExpr { public static final String NAME = "closest-point-on-polygon"; diff --git a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java index 4e4248ea52..6e668553b2 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java @@ -23,8 +23,8 @@ * * <p><strong>Parameters:</strong></p> * <ul> - * <li><code>polygon_coords</code>: A space-separated string of lon/lat pairs (e.g. "lon1 lat1 lon2 lat2 ...")</li> - * <li><code>point_coord</code>: A single point as "lon lat"</li> + * <li><code>polygon_coords</code>: A space-separated string of lat/lon pairs (e.g. "lat1 lon1 lat2 lon2 ...")</li> + * <li><code>point_coord</code>: A single point as "lat lon"</li> * </ul> * * <p><strong>Returns:</strong></p> @@ -33,9 +33,9 @@ * * <p><strong>Recommended Use:</strong></p> * <pre> - * is-point-inside-polygon('78.041309 27.174957','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78.041383 27.175569') + * is-point-inside-polygon('27.174957 78.041309 ','27.174957 78.041309 27.174884 78.042574 27.175493 78.042661 27.175569 78.041383') * </pre> - * <p>This example checks whether the point (78.041309, 27.174957) lies inside or on the polygon boundary. + * <p>This example checks whether the point (27.174957 78.041309) lies inside or on the polygon boundary. * It will return <code>true</code> because the point is exactly on one of the polygon's vertices.</p> */ public class XPathIsPointInsidePolygonFunc extends XPathFuncExpr { diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index 9fba88c124..cc1fd8e60b 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -610,58 +610,83 @@ public void doTests() { testEval("$var_double_five", null, varContext, Double.valueOf(5.0)); //Polygon point testEval( - "closest-point-on-polygon('78.041 27.176','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78" - + ".041383 27.175569')", - null, null, "78.041383 27.175569"); // Outside, near bottom-left vertex + "closest-point-on-polygon('27.176 78.041','27.174957 78.041309 27.174884 78.042574 27.175493 78.042661 27.175569 78.041383')", + null, null, "27.175569 78.041383"); // Outside, near bottom-left vertex + testEval( - "closest-point-on-polygon('78.041 27.176','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78" - + ".041383 27.175569')", - null, null, "78.041383 27.175569"); // Bottom-left vertex + "closest-point-on-polygon('27.176 78.041','27.174957 78.041309 27.174884 78.042574 27.175493 78.042661 27.175569 78.041383')", + null, null, "27.175569 78.041383"); testEval( - "closest-point-on-polygon('78.043 27.175','78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78" - + ".041383 27.175569')", - null, null, "78.04259876 27.175057319999997"); // Near top-right + "closest-point-on-polygon('27.175 78.043','27.174957 78.041309 27.174884 78.042574 27.175493 78.042661 27.175569 78.041383')", + null, null, "27.175057319999997 78.04259876"); // Near top-right edge - testEval("closest-point-on-polygon('78.042 27.175','78.041 27.174 78.043 27.174 78.043 27.176 78.041 27.176')", null, - null, "78.042 27.175"); // Inside polygon + testEval( + "closest-point-on-polygon('27.175 78.042','27.174 78.041 27.174 78.043 27.176 78.043 27.176 78.041')", + null, null, "27.175 78.042"); // Inside polygon - testEval("closest-point-on-polygon('78.042 27.177','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, - null, "78.042 27.176"); // Near top edge + testEval( + "closest-point-on-polygon('27.177 78.042','27.174 78.040 27.174 78.044 27.176 78.044 27.176 78.040')", + null, null, "27.176 78.042"); // Near top edge - testEval("closest-point-on-polygon('78.039 27.175','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, - null, "78.04 27.175"); // Left of polygon + testEval( + "closest-point-on-polygon('27.175 78.039','27.174 78.040 27.174 78.044 27.176 78.044 27.176 78.040')", + null, null, "27.175 78.04"); // Left of polygon - testEval("closest-point-on-polygon('78.045 27.1755','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, - null, "78.044 27.1755"); // Right side + testEval( + "closest-point-on-polygon('27.1755 78.045','27.174 78.040 27.174 78.044 27.176 78.044 27.176 78.040')", + null, null, "27.1755 78.044"); // Right side + + testEval( + "closest-point-on-polygon('27.173 78.042','27.174 78.040 27.174 78.044 27.176 78.044 27.176 78.040')", + null, null, "27.174 78.042"); // Bottom side - testEval("closest-point-on-polygon('78.042 27.173','78.040 27.174 78.044 27.174 78.044 27.176 78.040 27.176')", null, - null, "78.042 27.174"); // Bottom side //inside polygon testEval( - "is-point-inside-polygon('78.0195 27.204','78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27" - + ".2035281 78.0203027 27.2044155')", - null, null, true); // Inside the polygon + "is-point-inside-polygon('27.204 78.0195','27.2043773 78.0186987 27.203509 78.0187201 27.2035281 78.0202758 27.2044155 78.0203027')", + null, null, true); // Inside + + testEval( + "is-point-inside-polygon('27.2035 78.0205','27.2043773 78.0186987 27.203509 78.0187201 27.2035281 78.0202758 27.2044155 78.0203027')", + null, null, false); // Outside, near bottom-right + + testEval( + "is-point-inside-polygon('27.204 78.018','27.2043773 78.0186987 27.203509 78.0187201 27.2035281 78.0202758 27.2044155 78.0203027')", + null, null, false); // Outside, far left + + testEval( + "is-point-inside-polygon('27.203509 78.0187201','27.2043773 78.0186987 27.203509 78.0187201 27.2035281 78.0202758 27.2044155 78.0203027')", + null, null, true); // On vertex + + testEval( + "is-point-inside-polygon('27.203509 78.0187201','27.2043773 78.0186987 27.203509 78.0187201 27.2035281 78.0202758 27.2044155 78.0203027')", + null, null, true); // On vertex again + + testEval( + "closest-point-on-polygon('27.175 91.043','27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, "27.175057319999997 91.04259876"); + + testEval( + "closest-point-on-polygon('27.175 91.043','27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, "27.175057319999997 91.04259876"); + testEval( + "closest-point-on-polygon('27.176 91.043', '27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, "27.175493 91.042661"); + testEval( + "closest-point-on-polygon('27.175 91.040', '27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, "27.174957 91.041309"); + testEval( + "closest-point-on-polygon('27.175 91.044', '27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, "27.175197319999985 91.04261876"); testEval( - "is-point-inside-polygon('78.0205 27.2035','78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27" - + ".2035281 78.0203027 27.2044155')", - null, null, false); // Outside the polygon, near bottom-right + "closest-point-on-polygon('27.176 91.041', '27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, "27.175569 91.041383"); testEval( - "is-point-inside-polygon('78.018 27.204', '78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27" - + ".2035281 78.0203027 27.2044155')", - null, null, false); // Outside the polygon, far left + "closest-point-on-polygon('27.175 91.043', '27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, "27.175057319999997 91.04259876"); testEval( - "is-point-inside-polygon('78.0187201 27.203509', '78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155')", - null, null, true); // On the polygon vertex - - //Test Polygon - testEval("is-point-inside-polygon('1 1','0 0 2 2 0 2 2 0')", null, null, new XPathException( - "Invalid polygon: Self-intersection")); // Self-intersecting polygon - testEval("is-point-inside-polygon('78.0187201 27.203509','78.0186987 27.2043773 78.0187201')", null, null, - new XPathException("Invalid polygon: Self-intersection")); // Odd count, invalid input - testEval("is-point-inside-polygon('78.0187201 27.203509','78.0186987 27.2043773 78.0187201 27.203509')", null, null, - new XPathException( - "Invalid polygon: Self-intersection")); // Only 2 points, not a polygon + "closest-point-on-polygon('91.043 27.176','27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, "27.175569 91.041383"); //Attribute XPath References //testEval("/@blah", null, null, new XPathUnsupportedException()); From 66706e1f7016b3db66a2572aa2a656c2913a859c Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Thu, 12 Jun 2025 20:43:15 +0530 Subject: [PATCH 368/402] -nit pick --- .../javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java | 2 +- .../javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java | 2 +- src/test/java/org/javarosa/xpath/test/XPathEvalTest.java | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java index f2a8865966..825836393b 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java @@ -23,9 +23,9 @@ * * <p><strong>Parameters:</strong></p> * <ul> + * <li><code>point_coord</code>: A single point as "lat lon eg('27.175 78.043')"</li> * <li><code>polygon_coords</code>: A space-separated string of lat/lon pairs (e.g. "'27.174957 78.041309 27 * .174884 78.042574 27.175493 78.042661 27.175569 78.041383'")</li> - * <li><code>point_coord</code>: A single point as "lat lon eg('27.175 78.043')"</li> * </ul> * * <p><strong>Returns:</strong></p> diff --git a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java index 6e668553b2..02917c6139 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java @@ -23,8 +23,8 @@ * * <p><strong>Parameters:</strong></p> * <ul> + * <li><code>point_coord</code>: A single point as "lat lon"</li> * <li><code>polygon_coords</code>: A space-separated string of lat/lon pairs (e.g. "lat1 lon1 lat2 lon2 ...")</li> - * <li><code>point_coord</code>: A single point as "lat lon"</li> * </ul> * * <p><strong>Returns:</strong></p> diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index cc1fd8e60b..d8b4799c77 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -613,10 +613,6 @@ public void doTests() { "closest-point-on-polygon('27.176 78.041','27.174957 78.041309 27.174884 78.042574 27.175493 78.042661 27.175569 78.041383')", null, null, "27.175569 78.041383"); // Outside, near bottom-left vertex - testEval( - "closest-point-on-polygon('27.176 78.041','27.174957 78.041309 27.174884 78.042574 27.175493 78.042661 27.175569 78.041383')", - null, null, "27.175569 78.041383"); - testEval( "closest-point-on-polygon('27.175 78.043','27.174957 78.041309 27.174884 78.042574 27.175493 78.042661 27.175569 78.041383')", null, null, "27.175057319999997 78.04259876"); // Near top-right edge From 68637ca21d54eaeca024216f9fc1087f203f76f5 Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Fri, 13 Jun 2025 00:03:37 +0530 Subject: [PATCH 369/402] -fix lat lng test case --- src/test/java/org/javarosa/xpath/test/XPathEvalTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index d8b4799c77..6cb9bdb936 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -681,8 +681,8 @@ public void doTests() { "closest-point-on-polygon('27.175 91.043', '27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", null, null, "27.175057319999997 91.04259876"); testEval( - "closest-point-on-polygon('91.043 27.176','27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", - null, null, "27.175569 91.041383"); + "closest-point-on-polygon('27.176 91.043','27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, "27.175493 91.042661"); //Attribute XPath References //testEval("/@blah", null, null, new XPathUnsupportedException()); From f6379fc545df5a69950da568310ad00bc9442fb0 Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Fri, 13 Jun 2025 13:21:27 +0530 Subject: [PATCH 370/402] -check for coordinates --- .../org/javarosa/core/model/utils/PolygonUtils.java | 13 +++++++++++-- .../java/org/javarosa/xpath/test/XPathEvalTest.java | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java index 6f36358ecb..dc76137c88 100644 --- a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java @@ -34,6 +34,7 @@ public static Polygon createPolygon(List<String> latLngList) throws IllegalArgum double latitude = Double.parseDouble(latLngList.get(i * 2)); double longitude = Double.parseDouble(latLngList.get(i * 2 + 1)); coordinates[i] = new Coordinate(longitude, latitude); // JTS uses x=longitude, y=latitude + isValidCoordinates(coordinates[i].getY(), coordinates[i].getX()); } // Close the polygon @@ -50,6 +51,12 @@ public static Polygon createPolygon(List<String> latLngList) throws IllegalArgum return polygon; } + private static void isValidCoordinates(double latitude, double longitude) { + if ((latitude < -90.0 || latitude > 90.0) || (longitude < -180.0 || longitude > 180.0)) { + throw new IllegalArgumentException("Invalid polygon coordinates"); + } + } + /** * Determines if a given point lies inside or on the boundary of the provided polygon. * @@ -57,8 +64,9 @@ public static Polygon createPolygon(List<String> latLngList) throws IllegalArgum * @param pointData A {@link GeoPointData} representing the test point. * @return true if the point is inside or on the polygon, false otherwise. */ - public static boolean isPointInsideOrOnPolygon(GeoPointData pointData,Polygon polygon) { + public static boolean isPointInsideOrOnPolygon(GeoPointData pointData, Polygon polygon) throws IllegalArgumentException{ GeometryFactory geometryFactory = new GeometryFactory(); + isValidCoordinates(pointData.getLatitude(), pointData.getLongitude()); Point point = geometryFactory.createPoint( new Coordinate(pointData.getLongitude(), pointData.getLatitude())); return polygon.covers(point); @@ -71,8 +79,9 @@ public static boolean isPointInsideOrOnPolygon(GeoPointData pointData,Polygon po * @param pointData A {@link GeoPointData} representing the external point. * @return A string in the format "lat lng" representing the closest point on the polygon. */ - public static String getClosestPointOnPolygon(GeoPointData pointData,Polygon polygon) { + public static String getClosestPointOnPolygon(GeoPointData pointData, Polygon polygon) throws IllegalArgumentException { GeometryFactory geometryFactory = new GeometryFactory(); + isValidCoordinates(pointData.getLatitude(), pointData.getLongitude()); Point externalPoint = geometryFactory.createPoint( new Coordinate(pointData.getLongitude(), pointData.getLatitude())); Coordinate[] nearestPoints = DistanceOp.nearestPoints(polygon, externalPoint); diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index 6cb9bdb936..cc06d318e7 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -681,8 +681,8 @@ public void doTests() { "closest-point-on-polygon('27.175 91.043', '27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", null, null, "27.175057319999997 91.04259876"); testEval( - "closest-point-on-polygon('27.176 91.043','27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", - null, null, "27.175493 91.042661"); + "closest-point-on-polygon('27.176 91.043','91.041309 27.174957 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, new XPathException()); //Attribute XPath References //testEval("/@blah", null, null, new XPathUnsupportedException()); From 1b638e14c0c076a8a2295797453311ece8fc0141 Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Fri, 13 Jun 2025 13:45:39 +0530 Subject: [PATCH 371/402] - added more test case scenarios --- .../org/javarosa/xpath/test/XPathEvalTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index cc06d318e7..c84d20cc07 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -683,6 +683,24 @@ public void doTests() { testEval( "closest-point-on-polygon('27.176 91.043','91.041309 27.174957 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", null, null, new XPathException()); + testEval( + "closest-point-on-polygon('91.043 27.176','91.041309 27.174957 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, new XPathException()); + testEval( + "closest-point-on-polygon('91.043 27.176','27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, new XPathException()); + + testEval( + "closest-point-on-polygon('27.176 182.043','27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, new XPathException()); + + testEval( + "closest-point-on-polygon('27.176 -182.043','27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, new XPathException()); + + testEval( + "closest-point-on-polygon('27.176 91.043','27.174957 -184.056 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", + null, null, new XPathException()); //Attribute XPath References //testEval("/@blah", null, null, new XPathUnsupportedException()); From 54dce7f76da45ebd4a32883313f0861d03485c17 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Wed, 18 Jun 2025 10:23:37 +0700 Subject: [PATCH 372/402] Bump core corresponding to cc android master --- src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java b/src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java index 69646c9a2a..ce752f76cd 100644 --- a/src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java +++ b/src/cli/java/org/commcare/util/engine/CommCareConfigEngine.java @@ -70,7 +70,7 @@ public class CommCareConfigEngine { protected ArchiveFileRoot mArchiveRoot; public static final int MAJOR_VERSION = 2; - public static final int MINOR_VERSION = 57; + public static final int MINOR_VERSION = 58; public static final int MINIMAL_VERSION = 0; From fd643bc9052d7c9f807cca91216e98da29f1a59f Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Thu, 19 Jun 2025 18:46:33 +0530 Subject: [PATCH 373/402] -xpath implementation for map functions keeping earth curvature in mind --- build.gradle | 2 +- .../core/model/utils/PolygonUtils.java | 183 +++++++++++++----- .../expr/XPathClosestPointOnPolygonFunc.java | 20 +- .../expr/XPathIsPointInsidePolygonFunc.java | 24 ++- .../javarosa/xpath/test/XPathEvalTest.java | 37 ++-- 5 files changed, 185 insertions(+), 81 deletions(-) diff --git a/build.gradle b/build.gradle index 068bd05cc0..bed0313202 100644 --- a/build.gradle +++ b/build.gradle @@ -83,7 +83,7 @@ dependencies { implementation 'io.opentracing:opentracing-api:0.33.0' implementation 'io.opentracing:opentracing-util:0.33.0' implementation 'com.datadoghq:dd-trace-api:1.10.0' - implementation 'org.locationtech.jts:jts-core:1.19.0' + implementation 'org.gavaghan:geodesy:1.1.3' cliImplementation 'org.json:json:20140107' cliImplementation 'com.github.stefanhaustein:kxml2:2.4.1' cliImplementation 'commons-cli:commons-cli:1.3.1' diff --git a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java index dc76137c88..b9b32c0942 100644 --- a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java @@ -1,91 +1,174 @@ package org.javarosa.core.model.utils; +import org.gavaghan.geodesy.Ellipsoid; +import org.gavaghan.geodesy.GeodeticCalculator; +import org.gavaghan.geodesy.GeodeticCurve; +import org.gavaghan.geodesy.GlobalCoordinates; -import org.javarosa.core.model.data.GeoPointData; -import org.locationtech.jts.geom.*; -import org.locationtech.jts.operation.distance.DistanceOp; -import org.locationtech.jts.operation.valid.IsValidOp; - +import java.util.ArrayList; import java.util.List; /** - * Utility class for creating, validating, and interacting with geographic polygons. + * Utility class for creating, validating, and interacting with geographic polygons + * using geodesic (ellipsoid-aware) calculations. */ public class PolygonUtils { /** - * Creates a valid polygon from a list of latitude and longitude strings. + * Creates a polygon from a flat list of lat/lon strings. * - * @param latLngList List of alternating latitude and longitude strings (e.g., [lat1, lng1, lat2, lng2, ...]) - * @return a valid {@link Polygon} object - * @throws IllegalArgumentException if the input is malformed or the polygon is invalid + * @param latLngList Flat list of lat/lon values (e.g., [lat1, lon1, lat2, lon2, ...]) + * @return List of GlobalCoordinates representing the polygon (closed) + * @throws IllegalArgumentException if input is invalid or polygon is malformed */ - public static Polygon createPolygon(List<String> latLngList) throws IllegalArgumentException { + public static List<GlobalCoordinates> createPolygon(List<String> latLngList) throws IllegalArgumentException { if (latLngList == null || latLngList.size() < 6 || latLngList.size() % 2 != 0) { throw new IllegalArgumentException( - "Input must contain at least three lat/lng pairs (six elements total), and must be " - + "even-sized."); + "Input must contain at least three lat/lng pairs (six elements total), and must be even-sized."); } - GeometryFactory geometryFactory = new GeometryFactory(); int numPoints = latLngList.size() / 2; - Coordinate[] coordinates = new Coordinate[numPoints + 1]; + List<GlobalCoordinates> polygon = new ArrayList<>(); for (int i = 0; i < numPoints; i++) { - double latitude = Double.parseDouble(latLngList.get(i * 2)); - double longitude = Double.parseDouble(latLngList.get(i * 2 + 1)); - coordinates[i] = new Coordinate(longitude, latitude); // JTS uses x=longitude, y=latitude - isValidCoordinates(coordinates[i].getY(), coordinates[i].getX()); + double lat = Double.parseDouble(latLngList.get(i * 2)); + double lon = Double.parseDouble(latLngList.get(i * 2 + 1)); + isValidCoordinates(lat, lon); + polygon.add(new GlobalCoordinates(lat, lon)); } - // Close the polygon - coordinates[numPoints] = coordinates[0]; - - LinearRing shell = geometryFactory.createLinearRing(coordinates); - Polygon polygon = geometryFactory.createPolygon(shell); + // Close polygon if not already closed + if (!polygon.get(0).equals(polygon.get(polygon.size() - 1))) { + polygon.add(new GlobalCoordinates( + polygon.get(0).getLatitude(), + polygon.get(0).getLongitude())); + } - IsValidOp validator = new IsValidOp(polygon); - if (!validator.isValid()) { - throw new IllegalArgumentException("Invalid polygon: " + validator.getValidationError().getMessage()); + if (polygon.size() < 4) { + throw new IllegalArgumentException("Polygon must have at least three distinct vertices."); } return polygon; } - private static void isValidCoordinates(double latitude, double longitude) { + /** + * Checks if coordinates are within valid bounds for latitude and longitude. + * + * @param latitude Latitude in degrees + * @param longitude Longitude in degrees + * @throws IllegalArgumentException if values are outside geographic bounds + */ + public static void isValidCoordinates(double latitude, double longitude) { if ((latitude < -90.0 || latitude > 90.0) || (longitude < -180.0 || longitude > 180.0)) { throw new IllegalArgumentException("Invalid polygon coordinates"); } } /** - * Determines if a given point lies inside or on the boundary of the provided polygon. + * Computes the closest point on the polygon border to a given test point using geodesic projection. * - * @param polygon The {@link Polygon} to test against. - * @param pointData A {@link GeoPointData} representing the test point. - * @return true if the point is inside or on the polygon, false otherwise. + * @param point The input location + * @param polygon A closed list of polygon vertices + * @return String representation of the closest lat/lon pair */ - public static boolean isPointInsideOrOnPolygon(GeoPointData pointData, Polygon polygon) throws IllegalArgumentException{ - GeometryFactory geometryFactory = new GeometryFactory(); - isValidCoordinates(pointData.getLatitude(), pointData.getLongitude()); - Point point = geometryFactory.createPoint( - new Coordinate(pointData.getLongitude(), pointData.getLatitude())); - return polygon.covers(point); + public static String findClosestPoint(GlobalCoordinates point, List<GlobalCoordinates> polygon) { + GeodeticCalculator calc = new GeodeticCalculator(); + Ellipsoid ellipsoid = Ellipsoid.WGS84; + + double minDist = Double.MAX_VALUE; + GlobalCoordinates closest = null; + + for (int i = 0; i < polygon.size(); i++) { + GlobalCoordinates A = polygon.get(i); + GlobalCoordinates B = polygon.get((i + 1) % polygon.size()); + + GlobalCoordinates proj = projectOntoSegment(point, A, B, calc, ellipsoid); + if (proj == null) continue; + + GeodeticCurve curve = calc.calculateGeodeticCurve(ellipsoid, point, proj); + double dist = curve.getEllipsoidalDistance(); + + if (dist < minDist) { + minDist = dist; + closest = proj; + } + } + + return closest.getLatitude() + " " + closest.getLongitude(); } /** - * Finds the closest point on the polygon from a given external point. + * Projects a test point onto a geodesic segment between two polygon points. * - * @param polygon The {@link Polygon} to check against. - * @param pointData A {@link GeoPointData} representing the external point. - * @return A string in the format "lat lng" representing the closest point on the polygon. + * @param point Test point + * @param A Segment start + * @param B Segment end + * @param calc Geodetic calculator + * @param ellipsoid The ellipsoid reference (WGS84) + * @return Projected closest point on the segment */ - public static String getClosestPointOnPolygon(GeoPointData pointData, Polygon polygon) throws IllegalArgumentException { - GeometryFactory geometryFactory = new GeometryFactory(); - isValidCoordinates(pointData.getLatitude(), pointData.getLongitude()); - Point externalPoint = geometryFactory.createPoint( - new Coordinate(pointData.getLongitude(), pointData.getLatitude())); - Coordinate[] nearestPoints = DistanceOp.nearestPoints(polygon, externalPoint); - Coordinate closest = nearestPoints[0]; - return closest.y + " " + closest.x; // Return in "lat lng" format + private static GlobalCoordinates projectOntoSegment( + GlobalCoordinates point, + GlobalCoordinates A, + GlobalCoordinates B, + GeodeticCalculator calc, + Ellipsoid ellipsoid + ) { + if (A.getLatitude() == B.getLatitude() && A.getLongitude() == B.getLongitude()) { + return A; + } + + GeodeticCurve AB = calc.calculateGeodeticCurve(ellipsoid, A, B); + double azimuthAB = AB.getAzimuth(); + double totalLength = AB.getEllipsoidalDistance(); + + GeodeticCurve AP = calc.calculateGeodeticCurve(ellipsoid, A, point); + double azimuthAP = AP.getAzimuth(); + double distanceAP = AP.getEllipsoidalDistance(); + + double angleDiff = Math.toRadians(azimuthAP - azimuthAB); + double projection = distanceAP * Math.cos(angleDiff); + + if (projection <= 0) return A; + if (projection >= totalLength) return B; + + return calc.calculateEndingGlobalCoordinates(ellipsoid, A, azimuthAB, projection); + } + + /** + * Determines if a point lies inside or on the border of a polygon using the ray casting algorithm. + * + * @param point The point to test + * @param polygon The polygon (list of GlobalCoordinates) + * @return true if inside or on the edge; false otherwise + */ + public static boolean isPointInsideOrOnPolygon(GlobalCoordinates point, List<GlobalCoordinates> polygon) { + int intersectCount = 0; + int n = polygon.size(); + + double testLat = point.getLatitude(); + double testLon = point.getLongitude(); + + for (int i = 0; i < n; i++) { + GlobalCoordinates A = polygon.get(i); + GlobalCoordinates B = polygon.get((i + 1) % n); + + double lat1 = A.getLatitude(); + double lon1 = A.getLongitude(); + double lat2 = B.getLatitude(); + double lon2 = B.getLongitude(); + + // Vertex check + if ((testLat == lat1 && testLon == lon1) || (testLat == lat2 && testLon == lon2)) { + return true; + } + + // Ray casting + if (((lat1 > testLat) != (lat2 > testLat)) && + (testLon < (lon2 - lon1) * (testLat - lat1) / (lat2 - lat1 + 1e-10) + lon1)) { + intersectCount++; + } + } + + return (intersectCount % 2 == 1); } } \ No newline at end of file diff --git a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java index 825836393b..242b7defb8 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java @@ -1,5 +1,6 @@ package org.javarosa.xpath.expr; +import org.gavaghan.geodesy.GlobalCoordinates; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.data.GeoPointData; import org.javarosa.core.model.data.UncastData; @@ -8,9 +9,9 @@ import org.javarosa.xpath.XPathException; import org.javarosa.xpath.XPathTypeMismatchException; import org.javarosa.xpath.parser.XPathSyntaxException; -import org.locationtech.jts.geom.Polygon; import java.util.Arrays; +import java.util.List; /** * XPath function "closest-point-on-polygon()" computes the closest point on the boundary of a polygon @@ -66,18 +67,23 @@ private static String closestPointToPolygon(Object from, Object to) { String inputPoint = (String)FunctionUtils.unpack(from); String inputPolygon = (String)FunctionUtils.unpack(to); if (inputPoint == null || "".equals(inputPoint) || inputPolygon == null || "".equals(inputPolygon)) { - throw new XPathException("closest-point-on-polygon() function requires coordinates of point and polygon"); + throw new XPathException( + "closest-point-on-polygon() function requires coordinates of point and polygon"); } try { String[] coordinates = inputPolygon.split(" "); - Polygon polygon = PolygonUtils.createPolygon(Arrays.asList(coordinates)); + List<GlobalCoordinates> polygon = PolygonUtils.createPolygon(Arrays.asList(coordinates)); GeoPointData pointData = new GeoPointData().cast(new UncastData(inputPoint)); - return PolygonUtils.getClosestPointOnPolygon(pointData, polygon); + PolygonUtils.isValidCoordinates(pointData.getLatitude(), pointData.getLongitude()); + GlobalCoordinates pointCoordinates = new GlobalCoordinates(pointData.getLatitude(), + pointData.getLongitude()); + return PolygonUtils.findClosestPoint(pointCoordinates, polygon).toString(); } catch (NumberFormatException e) { - throw new XPathTypeMismatchException("closest-point-on-polygon() function requires arguments containing " + - "numeric values only, but received arguments: " + inputPoint + " and " + inputPolygon); + throw new XPathTypeMismatchException( + "closest-point-on-polygon() function requires arguments containing " + + "numeric values only, but received arguments: " + inputPoint + " and " + inputPolygon); } catch (IllegalArgumentException e) { throw new XPathException(e.getMessage()); } } -} +} \ No newline at end of file diff --git a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java index 02917c6139..68285c4de5 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java @@ -1,5 +1,6 @@ package org.javarosa.xpath.expr; +import org.gavaghan.geodesy.GlobalCoordinates; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.data.GeoPointData; import org.javarosa.core.model.data.UncastData; @@ -8,9 +9,9 @@ import org.javarosa.xpath.XPathException; import org.javarosa.xpath.XPathTypeMismatchException; import org.javarosa.xpath.parser.XPathSyntaxException; -import org.locationtech.jts.geom.Polygon; import java.util.Arrays; +import java.util.List; /** * XPath function "is-point-inside-polygon()" determines whether a geographic point lies inside @@ -24,7 +25,8 @@ * <p><strong>Parameters:</strong></p> * <ul> * <li><code>point_coord</code>: A single point as "lat lon"</li> - * <li><code>polygon_coords</code>: A space-separated string of lat/lon pairs (e.g. "lat1 lon1 lat2 lon2 ...")</li> + * <li><code>polygon_coords</code>: A space-separated string of lat/lon pairs (e.g. "lat1 lon1 lat2 lon2 ...") + * </li> * </ul> * * <p><strong>Returns:</strong></p> @@ -60,19 +62,23 @@ private static boolean isPointWithinBoundary(Object from, Object to) { String inputPoint = (String)FunctionUtils.unpack(from); String inputPolygon = (String)FunctionUtils.unpack(to); if (inputPoint == null || "".equals(inputPoint) || inputPolygon == null || "".equals(inputPolygon)) { - throw new XPathException("is-point-inside-polygon() function requires coordinates of point and polygon"); + throw new XPathException( + "is-point-inside-polygon() function requires coordinates of point and polygon"); } try { String[] coordinates = inputPolygon.split(" "); - Polygon polygon = PolygonUtils.createPolygon(Arrays.asList(coordinates)); + List<GlobalCoordinates> polygon = PolygonUtils.createPolygon(Arrays.asList(coordinates)); GeoPointData pointData = new GeoPointData().cast(new UncastData(inputPoint)); - - return PolygonUtils.isPointInsideOrOnPolygon(pointData, polygon); + PolygonUtils.isValidCoordinates(pointData.getLatitude(), pointData.getLongitude()); + GlobalCoordinates pointCoordinates = new GlobalCoordinates(pointData.getLatitude(), + pointData.getLongitude()); + return PolygonUtils.isPointInsideOrOnPolygon(pointCoordinates, polygon); } catch (NumberFormatException e) { - throw new XPathTypeMismatchException("is-point-inside-polygon() function requires arguments containing " + - "numeric values only, but received arguments: " + inputPoint + " and " + inputPolygon); + throw new XPathTypeMismatchException( + "is-point-inside-polygon() function requires arguments containing " + + "numeric values only, but received arguments: " + inputPoint + " and " + inputPolygon); } catch (IllegalArgumentException e) { throw new XPathException(e.getMessage()); } } -} +} \ No newline at end of file diff --git a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java index c84d20cc07..2b8f84f977 100755 --- a/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java +++ b/src/test/java/org/javarosa/xpath/test/XPathEvalTest.java @@ -611,31 +611,31 @@ public void doTests() { //Polygon point testEval( "closest-point-on-polygon('27.176 78.041','27.174957 78.041309 27.174884 78.042574 27.175493 78.042661 27.175569 78.041383')", - null, null, "27.175569 78.041383"); // Outside, near bottom-left vertex + null, null, "27.175568999999996 78.041383"); // Outside, near bottom-left vertex testEval( "closest-point-on-polygon('27.175 78.043','27.174957 78.041309 27.174884 78.042574 27.175493 78.042661 27.175569 78.041383')", - null, null, "27.175057319999997 78.04259876"); // Near top-right edge + null, null, "27.175046033871524 78.04259714760224"); // Near top-right edge testEval( "closest-point-on-polygon('27.175 78.042','27.174 78.041 27.174 78.043 27.176 78.043 27.176 78.041')", - null, null, "27.175 78.042"); // Inside polygon + null, null, "27.175000003564435 78.043"); // Inside polygon testEval( "closest-point-on-polygon('27.177 78.042','27.174 78.040 27.174 78.044 27.176 78.044 27.176 78.040')", - null, null, "27.176 78.042"); // Near top edge + null, null, "27.17600001425842 78.04200000000026"); // Near top edge testEval( "closest-point-on-polygon('27.175 78.039','27.174 78.040 27.174 78.044 27.176 78.044 27.176 78.040')", - null, null, "27.175 78.04"); // Left of polygon + null, null, "27.175000003564605 78.04000000000002"); // Left of polygon testEval( "closest-point-on-polygon('27.1755 78.045','27.174 78.040 27.174 78.044 27.176 78.044 27.176 78.040')", - null, null, "27.1755 78.044"); // Right side + null, null, "27.175500003564423 78.04399999999998"); // Right side testEval( "closest-point-on-polygon('27.173 78.042','27.174 78.040 27.174 78.044 27.176 78.044 27.176 78.040')", - null, null, "27.174 78.042"); // Bottom side + null, null, "27.1740000142577 78.0419999999998"); // Bottom side //inside polygon testEval( @@ -660,26 +660,26 @@ public void doTests() { testEval( "closest-point-on-polygon('27.175 91.043','27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", - null, null, "27.175057319999997 91.04259876"); + null, null, "27.175046033871524 91.04259714760224"); testEval( "closest-point-on-polygon('27.175 91.043','27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", - null, null, "27.175057319999997 91.04259876"); + null, null, "27.175046033871524 91.04259714760224"); testEval( "closest-point-on-polygon('27.176 91.043', '27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", - null, null, "27.175493 91.042661"); + null, null, "27.17549299999999 91.04266100000001"); testEval( "closest-point-on-polygon('27.175 91.040', '27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", - null, null, "27.174957 91.041309"); + null, null, "27.174957000000006 91.04130900000001"); testEval( "closest-point-on-polygon('27.175 91.044', '27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", - null, null, "27.175197319999985 91.04261876"); + null, null, "27.17515847323034 91.04261321034267"); testEval( "closest-point-on-polygon('27.176 91.041', '27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", - null, null, "27.175569 91.041383"); + null, null, "27.175568999999996 91.041383"); testEval( "closest-point-on-polygon('27.175 91.043', '27.174957 91.041309 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", - null, null, "27.175057319999997 91.04259876"); + null, null, "27.175046033871524 91.04259714760224"); testEval( "closest-point-on-polygon('27.176 91.043','91.041309 27.174957 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", null, null, new XPathException()); @@ -701,6 +701,15 @@ public void doTests() { testEval( "closest-point-on-polygon('27.176 91.043','27.174957 -184.056 27.174884 91.042574 27.175493 91.042661 27.175569 91.041383')", null, null, new XPathException()); + //Test cases for large polygons ~150km + testEval( + "closest-point-on-polygon('27.28 91.32', '27.20 91.10 27.20 91.30 27.40 91.30 27.40 91.10')", + null, null, "27.28000142694998 91.30000000000001" + ); + testEval( + "closest-point-on-polygon('27.35 91.30', '27.20 91.10 27.20 91.30 27.40 91.30 27.40 91.10')", + null, null, "27.349999999999994 91.30000000000001" + ); //Attribute XPath References //testEval("/@blah", null, null, new XPathUnsupportedException()); From 2849498d48f380b6343e82b9058644c29fc071f8 Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Sat, 21 Jun 2025 01:19:11 +0530 Subject: [PATCH 374/402] -changed variable names --- .../core/model/utils/PolygonUtils.java | 34 +++++++++---------- .../expr/XPathClosestPointOnPolygonFunc.java | 2 +- .../expr/XPathIsPointInsidePolygonFunc.java | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java index b9b32c0942..798663fe07 100644 --- a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java @@ -16,24 +16,24 @@ public class PolygonUtils { /** * Creates a polygon from a flat list of lat/lon strings. * - * @param latLngList Flat list of lat/lon values (e.g., [lat1, lon1, lat2, lon2, ...]) + * @param latLongList Flat list of lat/lon values (e.g., [lat1, lon1, lat2, lon2, ...]) * @return List of GlobalCoordinates representing the polygon (closed) * @throws IllegalArgumentException if input is invalid or polygon is malformed */ - public static List<GlobalCoordinates> createPolygon(List<String> latLngList) throws IllegalArgumentException { - if (latLngList == null || latLngList.size() < 6 || latLngList.size() % 2 != 0) { + public static List<GlobalCoordinates> createPolygon(List<String> latLongList) throws IllegalArgumentException { + if (latLongList == null || latLongList.size() < 6 || latLongList.size() % 2 != 0) { throw new IllegalArgumentException( "Input must contain at least three lat/lng pairs (six elements total), and must be even-sized."); } - int numPoints = latLngList.size() / 2; + int numPoints = latLongList.size() / 2; List<GlobalCoordinates> polygon = new ArrayList<>(); for (int i = 0; i < numPoints; i++) { - double lat = Double.parseDouble(latLngList.get(i * 2)); - double lon = Double.parseDouble(latLngList.get(i * 2 + 1)); - isValidCoordinates(lat, lon); - polygon.add(new GlobalCoordinates(lat, lon)); + double latitude = Double.parseDouble(latLongList.get(i * 2)); + double longitude = Double.parseDouble(latLongList.get(i * 2 + 1)); + validateCoordinates(latitude, longitude); + polygon.add(new GlobalCoordinates(latitude, longitude)); } // Close polygon if not already closed @@ -57,7 +57,7 @@ public static List<GlobalCoordinates> createPolygon(List<String> latLngList) thr * @param longitude Longitude in degrees * @throws IllegalArgumentException if values are outside geographic bounds */ - public static void isValidCoordinates(double latitude, double longitude) { + public static void validateCoordinates(double latitude, double longitude) { if ((latitude < -90.0 || latitude > 90.0) || (longitude < -180.0 || longitude > 180.0)) { throw new IllegalArgumentException("Invalid polygon coordinates"); } @@ -146,25 +146,25 @@ public static boolean isPointInsideOrOnPolygon(GlobalCoordinates point, List<Glo int n = polygon.size(); double testLat = point.getLatitude(); - double testLon = point.getLongitude(); + double testLong = point.getLongitude(); for (int i = 0; i < n; i++) { GlobalCoordinates A = polygon.get(i); GlobalCoordinates B = polygon.get((i + 1) % n); - double lat1 = A.getLatitude(); - double lon1 = A.getLongitude(); - double lat2 = B.getLatitude(); - double lon2 = B.getLongitude(); + double latA = A.getLatitude(); + double longA = A.getLongitude(); + double latB = B.getLatitude(); + double longB = B.getLongitude(); // Vertex check - if ((testLat == lat1 && testLon == lon1) || (testLat == lat2 && testLon == lon2)) { + if ((testLat == latA && testLong == longA) || (testLat == latB && testLong == longB)) { return true; } // Ray casting - if (((lat1 > testLat) != (lat2 > testLat)) && - (testLon < (lon2 - lon1) * (testLat - lat1) / (lat2 - lat1 + 1e-10) + lon1)) { + if (((latA > testLat) != (latB > testLat)) && + (testLong < (longB - longA) * (testLat - latA) / (latB - latA + 1e-10) + longA)) { intersectCount++; } } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java index 242b7defb8..dd65a67819 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java @@ -74,7 +74,7 @@ private static String closestPointToPolygon(Object from, Object to) { String[] coordinates = inputPolygon.split(" "); List<GlobalCoordinates> polygon = PolygonUtils.createPolygon(Arrays.asList(coordinates)); GeoPointData pointData = new GeoPointData().cast(new UncastData(inputPoint)); - PolygonUtils.isValidCoordinates(pointData.getLatitude(), pointData.getLongitude()); + PolygonUtils.validateCoordinates(pointData.getLatitude(), pointData.getLongitude()); GlobalCoordinates pointCoordinates = new GlobalCoordinates(pointData.getLatitude(), pointData.getLongitude()); return PolygonUtils.findClosestPoint(pointCoordinates, polygon).toString(); diff --git a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java index 68285c4de5..d9c39073dc 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java @@ -69,7 +69,7 @@ private static boolean isPointWithinBoundary(Object from, Object to) { String[] coordinates = inputPolygon.split(" "); List<GlobalCoordinates> polygon = PolygonUtils.createPolygon(Arrays.asList(coordinates)); GeoPointData pointData = new GeoPointData().cast(new UncastData(inputPoint)); - PolygonUtils.isValidCoordinates(pointData.getLatitude(), pointData.getLongitude()); + PolygonUtils.validateCoordinates(pointData.getLatitude(), pointData.getLongitude()); GlobalCoordinates pointCoordinates = new GlobalCoordinates(pointData.getLatitude(), pointData.getLongitude()); return PolygonUtils.isPointInsideOrOnPolygon(pointCoordinates, polygon); From 1c5607e4ec9890e1471b26d88afc0cb6d20ac609 Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Tue, 24 Jun 2025 16:54:21 +0530 Subject: [PATCH 375/402] -text change in contributing.md --- .github/contributing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/contributing.md b/.github/contributing.md index 4ac0117c46..b9db33bd66 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -21,9 +21,9 @@ To do this - ##### Duplicating A CommCare Android change to Formplayer -1. If you are working on a CommCare Android change, you will want to start by checking out `your_feature_branch` from `master` as the base branch. Make changes on `your_feature_branch` and create your original PR against `master` branch. +1. If you are working on a CommCare Android change, you have to start by checking out `your_feature_branch` from `master` as the base branch. Make changes on `your_feature_branch` and create your original PR against `master` branch. Get it approved and not merge it. -2. Now you will need to duplicate this PR by making another PR against `formplayer`. Make sure the branch for this PR is is merged and is not deleted. Then create the comment `duplicate this PR <starting-commit-id> <ending-commit-id>`. The `ending-commit-id` should be the last non-merge commit in the PR. This should result in a Github Actions workflow duplicating your PR against `formplayer`. Go to the duplicate PR, close and re-open it to run the Github checks against it. +2. Now you will need to duplicate this PR by making another PR against `formplayer`. For this create the comment `duplicate this PR <starting-commit-id> <ending-commit-id>` of the `your_feature_branch`. The `ending-commit-id` should be the last non-merge commit in the PR. This should result in a Github Actions workflow duplicating your PR against `formplayer`. Go to the duplicate PR, close and re-open it to run the Github checks against it. 3. In order for us to test that your PR against `formplayer` doesn't break anything on Formplayer, we need to run formplayer side tests with your PR. To do this - From 531db374ae874f0aecc617141bc4ad9bd92787de Mon Sep 17 00:00:00 2001 From: parthmittal <pmittal@dimagi-associate.com> Date: Tue, 24 Jun 2025 17:35:02 +0530 Subject: [PATCH 376/402] -text correction --- .github/contributing.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/contributing.md b/.github/contributing.md index b9db33bd66..dad8cb7c7f 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -21,11 +21,12 @@ To do this - ##### Duplicating A CommCare Android change to Formplayer -1. If you are working on a CommCare Android change, you have to start by checking out `your_feature_branch` from `master` as the base branch. Make changes on `your_feature_branch` and create your original PR against `master` branch. Get it approved and not merge it. +1. If you are working on a CommCare Android change, you have to start by checking out `your_feature_branch` from `master` as the base branch. Make changes on `your_feature_branch` and create your original PR against `master` branch. Get it approved and do not merge it. 2. Now you will need to duplicate this PR by making another PR against `formplayer`. For this create the comment `duplicate this PR <starting-commit-id> <ending-commit-id>` of the `your_feature_branch`. The `ending-commit-id` should be the last non-merge commit in the PR. This should result in a Github Actions workflow duplicating your PR against `formplayer`. Go to the duplicate PR, close and re-open it to run the Github checks against it. +3. After creating the successful duplicate PR we can merge the `your_feature_branch` to the `master` of Commcare. -3. In order for us to test that your PR against `formplayer` doesn't break anything on Formplayer, we need to run formplayer side tests with your PR. +4. In order for us to test that your PR against `formplayer` doesn't break anything on Formplayer, we need to run formplayer side tests with your PR. To do this - - Check out a new branch say `test_cc_1189` from `master` in [Formplayer](https://github.com/dimagi/formplayer) - Point the submodule in `libs/commcare` to your CommCare Core `your_feature_branch_dupe` branch and push your formplayer branch - From 15ac90fa0335e72d299f0ddc20936c3103471baa Mon Sep 17 00:00:00 2001 From: Jonathan Tang <Jonahtang738@gmail.com> Date: Thu, 19 Jun 2025 14:35:51 -0700 Subject: [PATCH 377/402] set casesearchstorage on queryscreen --- .../java/org/commcare/util/screen/QueryScreen.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/cli/java/org/commcare/util/screen/QueryScreen.java b/src/cli/java/org/commcare/util/screen/QueryScreen.java index 97c7d0ae0d..b672655e79 100644 --- a/src/cli/java/org/commcare/util/screen/QueryScreen.java +++ b/src/cli/java/org/commcare/util/screen/QueryScreen.java @@ -9,6 +9,7 @@ import com.google.common.collect.Multimap; +import org.commcare.cases.model.Case; import org.commcare.core.encryption.CryptUtil; import org.commcare.core.interfaces.VirtualDataInstanceStorage; import org.commcare.data.xml.VirtualInstances; @@ -24,6 +25,8 @@ import org.javarosa.core.services.locale.Localization; import org.javarosa.core.util.NoLocalizedTextException; import org.javarosa.core.util.OrderedHashtable; +import org.javarosa.core.services.storage.IStorageUtilityIndexed; +import org.commcare.cases.model.Case; import java.io.InputStream; import java.io.PrintStream; @@ -323,4 +326,12 @@ public SessionWrapper getSession() { public String toString() { return "QueryScreen[" + mTitle + "]"; } + + public void setCaseSearchStorage(IStorageUtilityIndexed<Case> caseSearchStorage) { + this.caseSearchStorage = caseSearchStorage; + } + + public IStorageUtilityIndexed<Case> getCaseSearchStorage() { + return caseSearchStorage; + } } From 8d960c4965a72c4333c36b0c937762424105b247 Mon Sep 17 00:00:00 2001 From: Jonathan Tang <Jonahtang738@gmail.com> Date: Thu, 19 Jun 2025 14:59:16 -0700 Subject: [PATCH 378/402] use the case search storage to get the case name if user sandbox does not contain the case --- .../commcare/util/screen/EntityScreen.java | 6 +++++- .../util/screen/MultiSelectEntityScreen.java | 7 ++++++- .../org/commcare/util/screen/QueryScreen.java | 3 ++- .../java/org/commcare/util/FormDataUtil.java | 21 +++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityScreen.java b/src/cli/java/org/commcare/util/screen/EntityScreen.java index 6ef70a4b2d..2c12938828 100755 --- a/src/cli/java/org/commcare/util/screen/EntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreen.java @@ -1,6 +1,7 @@ package org.commcare.util.screen; import org.commcare.cases.entity.EntityUtil; +import org.commcare.cases.model.Case; import org.commcare.cases.query.QueryContext; import org.commcare.cases.query.queryset.CurrentModelQuerySet; import org.commcare.core.interfaces.UserSandbox; @@ -18,6 +19,7 @@ import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.model.trace.EvaluationTraceReporter; import org.javarosa.core.model.utils.InstrumentationUtils; +import org.javarosa.core.services.storage.IStorageUtilityIndexed; import org.javarosa.core.util.NoLocalizedTextException; import java.util.Hashtable; @@ -268,7 +270,9 @@ public String getScreenTitle() { @Override public String getBreadcrumb(String input, UserSandbox sandbox, SessionWrapper session) { - String caseName = FormDataUtil.getCaseName(sandbox, input); + QueryScreen queryScreen = this.getQueryScreen(); + IStorageUtilityIndexed<Case> caseSearchStorage = queryScreen != null ? queryScreen.getCaseSearchStorage() : null; + String caseName = FormDataUtil.getCaseName(sandbox, caseSearchStorage, input); return caseName == null ? ScreenUtils.getBestTitle(session) : caseName; } diff --git a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java index e479c5518f..65b35d19d6 100644 --- a/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/MultiSelectEntityScreen.java @@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableMap; +import org.commcare.cases.model.Case; import org.commcare.core.interfaces.UserSandbox; import org.commcare.core.interfaces.VirtualDataInstanceStorage; import org.commcare.data.xml.VirtualInstances; @@ -20,6 +21,7 @@ import org.javarosa.core.model.instance.ExternalDataInstanceSource; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.services.locale.Localization; +import org.javarosa.core.services.storage.IStorageUtilityIndexed; import org.javarosa.core.util.NoLocalizedTextException; import javax.annotation.Nonnull; @@ -230,7 +232,10 @@ public String getBreadcrumb(String input, UserSandbox sandbox, SessionWrapper se int caseCount = root.getNumChildren(); if (caseCount > 0) { String caseId = root.getChildAt(0).getValue().getDisplayText(); - String caseName = FormDataUtil.getCaseName(sandbox, caseId); + String caseName = null; + QueryScreen queryScreen = this.getQueryScreen(); + IStorageUtilityIndexed<Case> caseSearchStorage = queryScreen != null ? queryScreen.getCaseSearchStorage() : null; + caseName = FormDataUtil.getCaseName(sandbox, caseSearchStorage, caseId); if (caseName != null) { if (caseCount > 1) { return "(" + caseCount + ") " + caseName + ", ..."; diff --git a/src/cli/java/org/commcare/util/screen/QueryScreen.java b/src/cli/java/org/commcare/util/screen/QueryScreen.java index b672655e79..7737244308 100644 --- a/src/cli/java/org/commcare/util/screen/QueryScreen.java +++ b/src/cli/java/org/commcare/util/screen/QueryScreen.java @@ -26,7 +26,6 @@ import org.javarosa.core.util.NoLocalizedTextException; import org.javarosa.core.util.OrderedHashtable; import org.javarosa.core.services.storage.IStorageUtilityIndexed; -import org.commcare.cases.model.Case; import java.io.InputStream; import java.io.PrintStream; @@ -67,6 +66,8 @@ public class QueryScreen extends Screen { private boolean dynamicSearch; private boolean searchOnClear; + private IStorageUtilityIndexed<Case> caseSearchStorage; + public QueryScreen(String domainedUsername, String password, PrintStream out, VirtualDataInstanceStorage instanceStorage, SessionUtils sessionUtils) { this.domainedUsername = domainedUsername; diff --git a/src/main/java/org/commcare/util/FormDataUtil.java b/src/main/java/org/commcare/util/FormDataUtil.java index 57f5a00e4e..8a02e2db25 100644 --- a/src/main/java/org/commcare/util/FormDataUtil.java +++ b/src/main/java/org/commcare/util/FormDataUtil.java @@ -10,6 +10,7 @@ import org.commcare.suite.model.Text; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.TreeReference; +import org.javarosa.core.services.storage.IStorageUtilityIndexed; /** * Use the session state descriptor attached to saved forms to load case @@ -104,4 +105,24 @@ public static String getCaseName(UserSandbox userSandbox, String caseId) { return null; } } + + public static String getCaseName(UserSandbox userSandbox, IStorageUtilityIndexed<Case> caseSearchStorage, String caseId) { + String caseName = getCaseName(userSandbox, caseId); + if (caseName != null) { + return caseName; + } + + if (caseSearchStorage != null && caseSearchStorage.isStorageExists()) { + try { + Case ourCase = caseSearchStorage.getRecordForValue(Case.INDEX_CASE_ID, caseId); + if (ourCase != null) { + return ourCase.getName(); + } + } catch (Exception searchException) { + return null; + } + } + + return null; + } } From 9319b5747ce3c7a882096238e50316b164481b1d Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna <avazirna@gmail.com> Date: Fri, 27 Jun 2025 11:45:59 +0200 Subject: [PATCH 379/402] Make calculation DST-aware --- src/main/java/org/javarosa/xform/util/CalendarUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/javarosa/xform/util/CalendarUtils.java b/src/main/java/org/javarosa/xform/util/CalendarUtils.java index 8ea46de208..ef27832e52 100644 --- a/src/main/java/org/javarosa/xform/util/CalendarUtils.java +++ b/src/main/java/org/javarosa/xform/util/CalendarUtils.java @@ -354,7 +354,7 @@ public static UniversalDate fromMillis(Date date, String timezone) { cd.setTimeZone(DateUtils.timezone()); } long dateInMillis = cd.getTime().getTime(); - DateTimeZone timezoneObject = DateTimeZone.forOffsetMillis(cd.getTimeZone().getRawOffset()); + DateTimeZone timezoneObject = DateTimeZone.forTimeZone(cd.getTimeZone()); return fromMillis(dateInMillis, timezoneObject); } From 310468d0d28c459b52afbde508ea8101a9700d1d Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna <avazirna@gmail.com> Date: Fri, 27 Jun 2025 11:55:19 +0200 Subject: [PATCH 380/402] Fix timezone offset calculation using Gregorian calendar --- src/main/java/org/javarosa/xform/util/CalendarUtils.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/javarosa/xform/util/CalendarUtils.java b/src/main/java/org/javarosa/xform/util/CalendarUtils.java index ef27832e52..f59d547eeb 100644 --- a/src/main/java/org/javarosa/xform/util/CalendarUtils.java +++ b/src/main/java/org/javarosa/xform/util/CalendarUtils.java @@ -419,7 +419,9 @@ public static long toMillisFromJavaEpoch(int year, int month, int day) { public static long toMillisFromJavaEpoch(int year, int month, int day, DateTimeZone currentTimeZone) { int daysFromMinDay = countDaysFromMinDay(year, month, day); long millisFromMinDay = daysFromMinDay * UniversalDate.MILLIS_IN_DAY; - int timezoneOffsetFromUTC = currentTimeZone.getOffset(millisFromMinDay); + + int timezoneOffsetFromUTC = currentTimeZone.getOffset(millisFromMinDay + MIN_MILLIS_FROM_JAVA_EPOCH); + long millisNormalizedToUTC = millisFromMinDay - timezoneOffsetFromUTC; return millisNormalizedToUTC + MIN_MILLIS_FROM_JAVA_EPOCH; } From 4c56611431308807cf31ef736efe92da29e67675 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna <avazirna@gmail.com> Date: Mon, 14 Jul 2025 23:14:30 +0200 Subject: [PATCH 381/402] Update the length of the Nepali months Bikram Sambat calendar is lunisolar, so month lenghts can change over time --- .../javarosa/xform/util/CalendarUtils.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/javarosa/xform/util/CalendarUtils.java b/src/main/java/org/javarosa/xform/util/CalendarUtils.java index f59d547eeb..310ffe4302 100644 --- a/src/main/java/org/javarosa/xform/util/CalendarUtils.java +++ b/src/main/java/org/javarosa/xform/util/CalendarUtils.java @@ -61,6 +61,7 @@ public static String ConvertToEthiopian(Date d, String format) { /* * Nepali calendar system has no discernible cyclic month pattern, so we must manually * enter them here as new calendars are known. + * Calendar source: https://nepalipatro.com.np/calendar * * TODO: Enter month lengths for years beyond 2090 */ @@ -176,16 +177,16 @@ public static String ConvertToEthiopian(Date d, String format) { NEPALI_YEAR_MONTHS.put(2078, new int[]{0, 31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30}); NEPALI_YEAR_MONTHS.put(2079, new int[]{0, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30}); NEPALI_YEAR_MONTHS.put(2080, new int[]{0, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30}); - NEPALI_YEAR_MONTHS.put(2081, new int[]{0, 31, 31, 32, 32, 31, 30, 30, 30, 29, 30, 30, 30}); - NEPALI_YEAR_MONTHS.put(2082, new int[]{0, 30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 30, 30}); - NEPALI_YEAR_MONTHS.put(2083, new int[]{0, 31, 31, 32, 31, 31, 30, 30, 30, 29, 30, 30, 30}); - NEPALI_YEAR_MONTHS.put(2084, new int[]{0, 31, 31, 32, 31, 31, 30, 30, 30, 29, 30, 30, 30}); - NEPALI_YEAR_MONTHS.put(2085, new int[]{0, 31, 32, 31, 32, 30, 31, 30, 30, 29, 30, 30, 30}); - NEPALI_YEAR_MONTHS.put(2086, new int[]{0, 30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 30, 30}); - NEPALI_YEAR_MONTHS.put(2087, new int[]{0, 31, 31, 32, 31, 31, 31, 30, 30, 29, 30, 30, 30}); - NEPALI_YEAR_MONTHS.put(2088, new int[]{0, 30, 31, 32, 32, 30, 31, 30, 30, 29, 30, 30, 30}); - NEPALI_YEAR_MONTHS.put(2089, new int[]{0, 30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 30, 30}); - NEPALI_YEAR_MONTHS.put(2090, new int[]{0, 30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 30, 30}); + NEPALI_YEAR_MONTHS.put(2081, new int[]{0, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31}); + NEPALI_YEAR_MONTHS.put(2082, new int[]{0, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30}); + NEPALI_YEAR_MONTHS.put(2083, new int[]{0, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30}); + NEPALI_YEAR_MONTHS.put(2084, new int[]{0, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31}); + NEPALI_YEAR_MONTHS.put(2085, new int[]{0, 30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31}); + NEPALI_YEAR_MONTHS.put(2086, new int[]{0, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30}); + NEPALI_YEAR_MONTHS.put(2087, new int[]{0, 31, 31, 32, 32, 31, 31, 30, 29, 30, 29, 30, 30}); + NEPALI_YEAR_MONTHS.put(2088, new int[]{0, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31}); + NEPALI_YEAR_MONTHS.put(2089, new int[]{0, 30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31}); + NEPALI_YEAR_MONTHS.put(2090, new int[]{0, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30}); } private static final int MAX_YEAR = 2090; From 51b01bcf514c55e13a11c9e73e702b7c03da5199 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Sat, 5 Oct 2024 13:03:50 +0200 Subject: [PATCH 382/402] Add test to demonstrate that widget works well with a timezone provider --- .../xform/util/test/CalendarTests.java | 70 ++++++++++++++++++- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/javarosa/xform/util/test/CalendarTests.java b/src/test/java/org/javarosa/xform/util/test/CalendarTests.java index 5f92ad89ca..48e5070b29 100644 --- a/src/test/java/org/javarosa/xform/util/test/CalendarTests.java +++ b/src/test/java/org/javarosa/xform/util/test/CalendarTests.java @@ -1,5 +1,9 @@ package org.javarosa.xform.util.test; +import org.javarosa.core.model.utils.DateUtils; +import org.javarosa.core.model.utils.TimezoneProvider; +import org.javarosa.core.services.locale.Localization; +import org.javarosa.core.services.locale.TableLocaleSource; import org.javarosa.xform.util.CalendarUtils; import org.javarosa.xform.util.UniversalDate; import org.joda.time.DateTimeZone; @@ -25,8 +29,10 @@ public void testTimesFallOnSameDate() { Calendar nepaliBeginningOfDayDate = Calendar.getInstance(nepaliTimeZone.toTimeZone()); nepaliBeginningOfDayDate.set(2007, Calendar.JULY, 7, 0, 0); - UniversalDate middleOfDay = CalendarUtils.fromMillis(nepaliMiddleOfDayDate.getTimeInMillis(), nepaliTimeZone); - UniversalDate beginningOfDay = CalendarUtils.fromMillis(nepaliBeginningOfDayDate.getTimeInMillis(), nepaliTimeZone); + UniversalDate middleOfDay = CalendarUtils.fromMillis(nepaliMiddleOfDayDate.getTimeInMillis(), + nepaliTimeZone); + UniversalDate beginningOfDay = CalendarUtils.fromMillis(nepaliBeginningOfDayDate.getTimeInMillis(), + nepaliTimeZone); assertSameDate(middleOfDay, beginningOfDay); Calendar nepaliEndOfDayDate = Calendar.getInstance(nepaliTimeZone.toTimeZone()); @@ -35,6 +41,45 @@ public void testTimesFallOnSameDate() { assertSameDate(endOfDay, beginningOfDay); } + // millis <=> date is different in every timezone + @Test + public void testConvertToNepaliString() { + configureLocaleForCalendar(); + MockTimeZoneProvider mockTimeZoneProvider = new MockTimeZoneProvider(TimeZone.getTimeZone("Europe/Madrid")); + DateUtils.setTimezoneProvider(mockTimeZoneProvider); + DateTimeZone timeZone = DateTimeZone.forOffsetMillis(mockTimeZoneProvider.getTimezone().getRawOffset()); + // this is what Nepali widget uses to calculate the millis from date fields + long millis = CalendarUtils.toMillisFromJavaEpoch(2081, 6, 16, timeZone); + String nepaliDateStr = CalendarUtils.convertToNepaliString(new Date(millis), null); + assertEquals( "16 Ashwin 2081", nepaliDateStr); + + + mockTimeZoneProvider.setTimezone(TimeZone.getTimeZone("Asia/Kathmandu")); + timeZone = DateTimeZone.forOffsetMillis(mockTimeZoneProvider.getTimezone().getRawOffset()); + millis = CalendarUtils.toMillisFromJavaEpoch(2081, 6, 16, timeZone); + nepaliDateStr = CalendarUtils.convertToNepaliString(new Date(millis), null); + assertEquals( "16 Ashwin 2081", nepaliDateStr); + + + mockTimeZoneProvider.setTimezone(TimeZone.getTimeZone("America/Chicago")); + timeZone = DateTimeZone.forOffsetMillis(mockTimeZoneProvider.getTimezone().getRawOffset()); + millis = CalendarUtils.toMillisFromJavaEpoch(2081, 6, 16, timeZone); + nepaliDateStr = CalendarUtils.convertToNepaliString(new Date(millis), null); + assertEquals( "16 Ashwin 2081", nepaliDateStr); + DateUtils.setTimezoneProvider(null); + } + + private void configureLocaleForCalendar() { + Localization.getGlobalLocalizerAdvanced().addAvailableLocale("default"); + Localization.setLocale("default"); + TableLocaleSource localeData = new TableLocaleSource(); + localeData.setLocaleMapping("ethiopian_months", + "Mäskäräm,T’ïk’ïmt,Hïdar,Tahsas,T’ïr,Yäkatit,Mägabit,Miyaziya,Gïnbot,Säne,Hämle,Nähäse,P’agume"); + localeData.setLocaleMapping("nepali_months", + "Baishakh,Jestha,Ashadh,Shrawan,Bhadra,Ashwin,Kartik,Mangsir,Poush,Magh,Falgun,Chaitra"); + Localization.getGlobalLocalizerAdvanced().registerLocaleResource("default", localeData); + } + private static void assertSameDate(UniversalDate a, UniversalDate b) { assertEquals(a.day, b.day); assertEquals(a.month, b.month); @@ -63,7 +108,8 @@ public void testUnpackingDateInDifferentTimezone() { mexicoCal.set(2007, Calendar.JULY, 7, 18, 46); UniversalDate mexicanDate = CalendarUtils.fromMillis(mexicoCal.getTimeInMillis(), mexicanTimeZone); - long time = CalendarUtils.toMillisFromJavaEpoch(mexicanDate.year, mexicanDate.month, mexicanDate.day, mexicanTimeZone); + long time = CalendarUtils.toMillisFromJavaEpoch(mexicanDate.year, mexicanDate.month, mexicanDate.day, + mexicanTimeZone); UniversalDate rebuiltDateInUsingDifferentTimezone = CalendarUtils.fromMillis(time, nepaliTimeZone); assertSameDate(rebuiltDateInUsingDifferentTimezone, mexicanDate); } @@ -103,4 +149,22 @@ public void serializeUniversalDateViaMillisTest() { deserializedNepaliDate = CalendarUtils.fromMillis(date.getTime(), bostonTimeZone); assertSameDate(nepaliDate, deserializedNepaliDate); } + + private class MockTimeZoneProvider extends TimezoneProvider { + + private TimeZone timeZone; + + public MockTimeZoneProvider(TimeZone timeZone) { + this.timeZone = timeZone; + } + + public void setTimezone(TimeZone timeZone){ + this.timeZone = timeZone; + } + + @Override + public TimeZone getTimezone() { + return timeZone; + } + } } From dcf459cbb242b2dbf898c531315d415c1067533b Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna <avazirna@gmail.com> Date: Tue, 15 Jul 2025 09:47:55 +0200 Subject: [PATCH 383/402] Refactor --- .../xform/util/test/CalendarTests.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/test/java/org/javarosa/xform/util/test/CalendarTests.java b/src/test/java/org/javarosa/xform/util/test/CalendarTests.java index 48e5070b29..f2f62a801e 100644 --- a/src/test/java/org/javarosa/xform/util/test/CalendarTests.java +++ b/src/test/java/org/javarosa/xform/util/test/CalendarTests.java @@ -7,6 +7,7 @@ import org.javarosa.xform.util.CalendarUtils; import org.javarosa.xform.util.UniversalDate; import org.joda.time.DateTimeZone; +import org.junit.Before; import org.junit.Test; import java.util.Calendar; @@ -19,6 +20,19 @@ * @author Phillip Mates (pmates@dimagi.com) */ public class CalendarTests { + + @Before + public void configureLocaleForCalendar() { + Localization.getGlobalLocalizerAdvanced().addAvailableLocale("default"); + Localization.setLocale("default"); + TableLocaleSource localeData = new TableLocaleSource(); + localeData.setLocaleMapping("ethiopian_months", + "Mäskäräm,T’ïk’ïmt,Hïdar,Tahsas,T’ïr,Yäkatit,Mägabit,Miyaziya,Gïnbot,Säne,Hämle,Nähäse,P’agume"); + localeData.setLocaleMapping("nepali_months", + "Baishakh,Jestha,Ashadh,Shrawan,Bhadra,Ashwin,Kartik,Mangsir,Poush,Magh,Falgun,Chaitra"); + Localization.getGlobalLocalizerAdvanced().registerLocaleResource("default", localeData); + } + @Test public void testTimesFallOnSameDate() { DateTimeZone nepaliTimeZone = DateTimeZone.forTimeZone(TimeZone.getTimeZone("GMT+05:45")); @@ -44,7 +58,6 @@ public void testTimesFallOnSameDate() { // millis <=> date is different in every timezone @Test public void testConvertToNepaliString() { - configureLocaleForCalendar(); MockTimeZoneProvider mockTimeZoneProvider = new MockTimeZoneProvider(TimeZone.getTimeZone("Europe/Madrid")); DateUtils.setTimezoneProvider(mockTimeZoneProvider); DateTimeZone timeZone = DateTimeZone.forOffsetMillis(mockTimeZoneProvider.getTimezone().getRawOffset()); @@ -69,17 +82,6 @@ public void testConvertToNepaliString() { DateUtils.setTimezoneProvider(null); } - private void configureLocaleForCalendar() { - Localization.getGlobalLocalizerAdvanced().addAvailableLocale("default"); - Localization.setLocale("default"); - TableLocaleSource localeData = new TableLocaleSource(); - localeData.setLocaleMapping("ethiopian_months", - "Mäskäräm,T’ïk’ïmt,Hïdar,Tahsas,T’ïr,Yäkatit,Mägabit,Miyaziya,Gïnbot,Säne,Hämle,Nähäse,P’agume"); - localeData.setLocaleMapping("nepali_months", - "Baishakh,Jestha,Ashadh,Shrawan,Bhadra,Ashwin,Kartik,Mangsir,Poush,Magh,Falgun,Chaitra"); - Localization.getGlobalLocalizerAdvanced().registerLocaleResource("default", localeData); - } - private static void assertSameDate(UniversalDate a, UniversalDate b) { assertEquals(a.day, b.day); assertEquals(a.month, b.month); From e831bea0834bb162a8b0ac264857003736c51104 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna <avazirna@gmail.com> Date: Tue, 15 Jul 2025 12:20:04 +0200 Subject: [PATCH 384/402] Reset TZ provider post unit testing --- src/test/java/org/javarosa/xform/util/test/CalendarTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/javarosa/xform/util/test/CalendarTests.java b/src/test/java/org/javarosa/xform/util/test/CalendarTests.java index f2f62a801e..d9bf703845 100644 --- a/src/test/java/org/javarosa/xform/util/test/CalendarTests.java +++ b/src/test/java/org/javarosa/xform/util/test/CalendarTests.java @@ -79,7 +79,7 @@ public void testConvertToNepaliString() { millis = CalendarUtils.toMillisFromJavaEpoch(2081, 6, 16, timeZone); nepaliDateStr = CalendarUtils.convertToNepaliString(new Date(millis), null); assertEquals( "16 Ashwin 2081", nepaliDateStr); - DateUtils.setTimezoneProvider(null); + DateUtils.resetTimezoneProvider(); } private static void assertSameDate(UniversalDate a, UniversalDate b) { From 334a9006d6b1cbb691b005c2d9ee38c1c5dd759f Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna <avazirna@gmail.com> Date: Wed, 23 Jul 2025 10:55:17 +0200 Subject: [PATCH 385/402] Update tests to account for DST --- .../java/org/javarosa/xform/util/test/CalendarTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/javarosa/xform/util/test/CalendarTests.java b/src/test/java/org/javarosa/xform/util/test/CalendarTests.java index d9bf703845..a8c02a2f5a 100644 --- a/src/test/java/org/javarosa/xform/util/test/CalendarTests.java +++ b/src/test/java/org/javarosa/xform/util/test/CalendarTests.java @@ -60,7 +60,7 @@ public void testTimesFallOnSameDate() { public void testConvertToNepaliString() { MockTimeZoneProvider mockTimeZoneProvider = new MockTimeZoneProvider(TimeZone.getTimeZone("Europe/Madrid")); DateUtils.setTimezoneProvider(mockTimeZoneProvider); - DateTimeZone timeZone = DateTimeZone.forOffsetMillis(mockTimeZoneProvider.getTimezone().getRawOffset()); + DateTimeZone timeZone = DateTimeZone.forTimeZone(mockTimeZoneProvider.timeZone); // this is what Nepali widget uses to calculate the millis from date fields long millis = CalendarUtils.toMillisFromJavaEpoch(2081, 6, 16, timeZone); String nepaliDateStr = CalendarUtils.convertToNepaliString(new Date(millis), null); @@ -68,14 +68,14 @@ public void testConvertToNepaliString() { mockTimeZoneProvider.setTimezone(TimeZone.getTimeZone("Asia/Kathmandu")); - timeZone = DateTimeZone.forOffsetMillis(mockTimeZoneProvider.getTimezone().getRawOffset()); + timeZone = DateTimeZone.forTimeZone(mockTimeZoneProvider.timeZone); millis = CalendarUtils.toMillisFromJavaEpoch(2081, 6, 16, timeZone); nepaliDateStr = CalendarUtils.convertToNepaliString(new Date(millis), null); assertEquals( "16 Ashwin 2081", nepaliDateStr); mockTimeZoneProvider.setTimezone(TimeZone.getTimeZone("America/Chicago")); - timeZone = DateTimeZone.forOffsetMillis(mockTimeZoneProvider.getTimezone().getRawOffset()); + timeZone = DateTimeZone.forTimeZone(mockTimeZoneProvider.timeZone); millis = CalendarUtils.toMillisFromJavaEpoch(2081, 6, 16, timeZone); nepaliDateStr = CalendarUtils.convertToNepaliString(new Date(millis), null); assertEquals( "16 Ashwin 2081", nepaliDateStr); From 693ec3d940c9d8826a497af2b40671cf222d7227 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna <avazirna@gmail.com> Date: Wed, 23 Jul 2025 11:09:17 +0200 Subject: [PATCH 386/402] Bump JSON to version 20250517 --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index fa00c68b32..0784c5bb95 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ dependencies { api 'com.github.stefanhaustein:kxml2:2.4.1' implementation 'javax.ws.rs:javax.ws.rs-api:2.0.1' // Added so Android Studio recognizes libs in util jar projects - implementation 'org.json:json:20250107' + implementation 'org.json:json:20250517' implementation 'commons-cli:commons-cli:1.3.1' implementation 'joda-time:joda-time:2.12.1' implementation 'com.carrotsearch:hppc:0.9.1' @@ -84,7 +84,7 @@ dependencies { implementation 'io.opentracing:opentracing-util:0.33.0' implementation 'com.datadoghq:dd-trace-api:1.10.0' - cliImplementation 'org.json:json:20250107' + cliImplementation 'org.json:json:20250517' cliImplementation 'com.github.stefanhaustein:kxml2:2.4.1' cliImplementation 'commons-cli:commons-cli:1.3.1' cliImplementation 'joda-time:joda-time:2.9.4' @@ -99,7 +99,7 @@ dependencies { testImplementation sourceSets.cli.output testImplementation sourceSets.translate.output testImplementation 'junit:junit:4.13.2' - testImplementation 'org.json:json:20250107' + testImplementation 'org.json:json:20250517' testImplementation 'org.xerial:sqlite-jdbc:3.40.0.0' testImplementation 'joda-time:joda-time:2.9.4' testImplementation 'com.squareup.retrofit2:retrofit:2.3.0' @@ -107,7 +107,7 @@ dependencies { testImplementation 'com.google.guava:guava:31.1-jre' ccapiImplementation sourceSets.main.output - ccapiImplementation 'org.json:json:20250107' + ccapiImplementation 'org.json:json:20250517' ccapiImplementation 'org.xerial:sqlite-jdbc:3.40.0.0' ccapiImplementation 'com.github.stefanhaustein:kxml2:2.4.1' ccapiImplementation 'joda-time:joda-time:2.9.4' @@ -125,7 +125,7 @@ dependencies { translateImplementation 'commons-cli:commons-cli:1.3.1' translateImplementation files('lib/json-simple-1.1.1.jar') translateImplementation 'joda-time:joda-time:2.9.4' - translateImplementation 'org.json:json:20250107' + translateImplementation 'org.json:json:20250517' translateImplementation 'io.reactivex.rxjava2:rxjava:2.1.1' translateImplementation "com.squareup.retrofit2:retrofit:2.3.0" translateImplementation 'com.squareup.okhttp3:okhttp:4.11.0' From 5bc66ec70261664d6a1fec1d7df4c94ef0e0fe2b Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna <avazirna@gmail.com> Date: Fri, 25 Jul 2025 18:48:02 +0200 Subject: [PATCH 387/402] Revert "Fix timezone offset calculation using Gregorian calendar" This reverts commit 310468d0d28c459b52afbde508ea8101a9700d1d. --- src/main/java/org/javarosa/xform/util/CalendarUtils.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/javarosa/xform/util/CalendarUtils.java b/src/main/java/org/javarosa/xform/util/CalendarUtils.java index 310ffe4302..79fa4da5e7 100644 --- a/src/main/java/org/javarosa/xform/util/CalendarUtils.java +++ b/src/main/java/org/javarosa/xform/util/CalendarUtils.java @@ -420,9 +420,7 @@ public static long toMillisFromJavaEpoch(int year, int month, int day) { public static long toMillisFromJavaEpoch(int year, int month, int day, DateTimeZone currentTimeZone) { int daysFromMinDay = countDaysFromMinDay(year, month, day); long millisFromMinDay = daysFromMinDay * UniversalDate.MILLIS_IN_DAY; - - int timezoneOffsetFromUTC = currentTimeZone.getOffset(millisFromMinDay + MIN_MILLIS_FROM_JAVA_EPOCH); - + int timezoneOffsetFromUTC = currentTimeZone.getOffset(millisFromMinDay); long millisNormalizedToUTC = millisFromMinDay - timezoneOffsetFromUTC; return millisNormalizedToUTC + MIN_MILLIS_FROM_JAVA_EPOCH; } From c698cd148a38c38af6a88c5ff2f04237d5902ffb Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Thu, 28 Aug 2025 12:47:42 +0530 Subject: [PATCH 388/402] Adds a model for apps to define credentials --- .../org/commcare/suite/model/Credential.java | 59 +++++++++++++++++++ .../org/commcare/suite/model/Profile.java | 13 ++++ .../java/org/commcare/xml/ProfileParser.java | 27 +++++++++ .../suite/model/test/ProfileTests.java | 11 ++++ src/test/resources/basic_profile.ccpr | 5 ++ 5 files changed, 115 insertions(+) create mode 100644 src/main/java/org/commcare/suite/model/Credential.java diff --git a/src/main/java/org/commcare/suite/model/Credential.java b/src/main/java/org/commcare/suite/model/Credential.java new file mode 100644 index 0000000000..600b55ec7b --- /dev/null +++ b/src/main/java/org/commcare/suite/model/Credential.java @@ -0,0 +1,59 @@ +package org.commcare.suite.model; + +import org.javarosa.core.util.externalizable.DeserializationException; +import org.javarosa.core.util.externalizable.ExtUtil; +import org.javarosa.core.util.externalizable.Externalizable; +import org.javarosa.core.util.externalizable.PrototypeFactory; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Apps use this model to convey the types of credentials it issues + */ +public class Credential implements Externalizable { + + private String level; + private String type; + + /** + * Serialization Only!!! + */ + public Credential() { + } + + public Credential(String level, String type) { + this.level = level; + this.type = type; + } + + @Override + public void readExternal(DataInputStream in, PrototypeFactory pf) + throws IOException, DeserializationException { + level = ExtUtil.readString(in); + type = ExtUtil.readString(in); + } + + @Override + public void writeExternal(DataOutputStream out) throws IOException { + ExtUtil.writeString(out, level); + ExtUtil.writeString(out, type); + } + + public String getLevel() { + return level; + } + + public String getType() { + return type; + } + + @Override + public String toString() { + return "Credential{" + + "level='" + level + '\'' + + ", type='" + type + '\'' + + '}'; + } +} diff --git a/src/main/java/org/commcare/suite/model/Profile.java b/src/main/java/org/commcare/suite/model/Profile.java index 92d535a0b4..42dfa6de5e 100644 --- a/src/main/java/org/commcare/suite/model/Profile.java +++ b/src/main/java/org/commcare/suite/model/Profile.java @@ -48,6 +48,7 @@ public class Profile implements Persistable { * were added for multiple app seating functionality */ private boolean fromOld; + private Vector<Credential> credentials; @SuppressWarnings("unused") public Profile() { @@ -77,6 +78,7 @@ public Profile(int version, String authRef, String uniqueId, String displayName, properties = new Vector<>(); roots = new Vector<>(); dependencies = new Vector<>(); + credentials = new Vector<>(); featureStatus = new Hashtable<>(); //turn on default features featureStatus.put("users", true); @@ -191,6 +193,14 @@ public void setDependencies(Vector<AndroidPackageDependency> dependencies) { this.dependencies = dependencies; } + public void setCredentials(Vector<Credential> credentials) { + this.credentials = credentials; + } + + public Vector<Credential> getCredentials() { + return credentials; + } + /** * A helper method which initializes the properties specified * by this profile definition. @@ -228,6 +238,8 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) buildProfileId = ExtUtil.readString(in); dependencies = (Vector<AndroidPackageDependency>)ExtUtil.read(in, new ExtWrapList(AndroidPackageDependency.class), pf); + credentials = (Vector<Credential>)ExtUtil.read(in, + new ExtWrapList(Credential.class), pf); } @Override @@ -244,5 +256,6 @@ public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.write(out, new ExtWrapMap(featureStatus)); ExtUtil.writeString(out, buildProfileId); ExtUtil.write(out, new ExtWrapList(dependencies)); + ExtUtil.write(out, new ExtWrapList(credentials)); } } diff --git a/src/main/java/org/commcare/xml/ProfileParser.java b/src/main/java/org/commcare/xml/ProfileParser.java index 54e2183976..6eed7d6975 100644 --- a/src/main/java/org/commcare/xml/ProfileParser.java +++ b/src/main/java/org/commcare/xml/ProfileParser.java @@ -6,6 +6,7 @@ import org.commcare.resources.model.Resource; import org.commcare.resources.model.ResourceTable; import org.commcare.suite.model.AndroidPackageDependency; +import org.commcare.suite.model.Credential; import org.commcare.suite.model.Profile; import org.commcare.util.CommCarePlatform; import org.javarosa.core.reference.RootTranslator; @@ -27,7 +28,11 @@ public class ProfileParser extends ElementParser<Profile> { private static final String NAME_DEPENDENCIES = "dependencies"; private static final String NAME_ANDROID_PACKAGE = "android_package"; + private static final String NAME_CREDENTIALS = "credentials"; + private static final String NAME_CREDENTIAL = "credential"; private static final String ATTR_ID = "id"; + private static final String ATTR_CREDENTIAL_LEVEL = "level"; + private static final String ATTR_CREDENTIAL_TYPE = "type"; ResourceTable table; String resourceId; @@ -244,6 +249,8 @@ private void parseFeatures(Profile profile) throws XmlPullParserException, IOExc } else if (tag.equals(NAME_DEPENDENCIES)) { profile.setDependencies(parseDependencies()); } else if (tag.equals("sense")) { + }else if (tag.equals(NAME_CREDENTIALS)) { + profile.setCredentials(parseCredentials()); } profile.setFeatureActive(tag, isActive); @@ -251,6 +258,26 @@ private void parseFeatures(Profile profile) throws XmlPullParserException, IOExc } } + private Vector<Credential> parseCredentials() + throws InvalidStructureException, XmlPullParserException, IOException { + Vector<Credential> appCredentials = new Vector<>(); + while (nextTagInBlock(NAME_CREDENTIALS)) { + String tag = parser.getName().toLowerCase(); + if (tag.equals(NAME_CREDENTIAL)) { + String level = parser.getAttributeValue(null, ATTR_CREDENTIAL_LEVEL); + String type = parser.getAttributeValue(null, ATTR_CREDENTIAL_TYPE); + if (level == null) { + throw new InvalidStructureException("No level defined for credential"); + } + if (type == null) { + throw new InvalidStructureException("No type defined for credential"); + } + appCredentials.add(new Credential(level, type)); + } + } + return appCredentials; + } + private Vector<AndroidPackageDependency> parseDependencies() throws InvalidStructureException, XmlPullParserException, IOException { Vector<AndroidPackageDependency> appDependencies = new Vector<>(); diff --git a/src/test/java/org/commcare/backend/suite/model/test/ProfileTests.java b/src/test/java/org/commcare/backend/suite/model/test/ProfileTests.java index 3493c724d2..29cb575b8d 100644 --- a/src/test/java/org/commcare/backend/suite/model/test/ProfileTests.java +++ b/src/test/java/org/commcare/backend/suite/model/test/ProfileTests.java @@ -9,6 +9,7 @@ import org.commcare.resources.model.Resource; import org.commcare.resources.model.ResourceTable; import org.commcare.suite.model.AndroidPackageDependency; +import org.commcare.suite.model.Credential; import org.commcare.suite.model.Profile; import org.commcare.test.utilities.PersistableSandbox; import org.commcare.util.engine.CommCareConfigEngine; @@ -92,6 +93,16 @@ public void testDependenciesParse() { assertEquals(Arrays.toString(expectedDependencies),Arrays.toString(p.getDependencies().toArray())); } + @Test + public void testCredentialsParse() { + Profile p = getProfile(BASIC_PROFILE_PATH); + assertTrue(p.isFeatureActive("credentials")); + Credential[] expectedCredentials = new Credential[2]; + expectedCredentials[0] = new Credential("3MON_ACTIVE", "APP_ACTIVITY"); + expectedCredentials[1] = new Credential("6MON_ACTIVE", "APP_ACTIVITY"); + assertEquals(Arrays.toString(expectedCredentials),Arrays.toString(p.getCredentials().toArray())); + } + private void compareProfiles(Profile a, Profile b) { if(!ArrayUtilities.arraysEqual(a.getPropertySetters(), b.getPropertySetters())) { fail("Mismatch of property setters between profiles"); diff --git a/src/test/resources/basic_profile.ccpr b/src/test/resources/basic_profile.ccpr index ff9f0c2dfa..0fd3095017 100644 --- a/src/test/resources/basic_profile.ccpr +++ b/src/test/resources/basic_profile.ccpr @@ -91,6 +91,11 @@ <android_package id="org.commcare.test"/> </dependencies> + <credentials active="true"> + <credential level="3MON_ACTIVE" type="APP_ACTIVITY"></credential> + <credential level="6MON_ACTIVE" type="APP_ACTIVITY"></credential> + </credentials> + </features> <suite><resource id="suite" version="102"> From 0cb1240312355a4671bce61224e4e4db752cd0f3 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Mon, 8 Sep 2025 15:33:01 +0530 Subject: [PATCH 389/402] private -> public --- src/main/java/org/commcare/resources/model/ResourceTable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/commcare/resources/model/ResourceTable.java b/src/main/java/org/commcare/resources/model/ResourceTable.java index 11a06d761d..38b9e8db15 100644 --- a/src/main/java/org/commcare/resources/model/ResourceTable.java +++ b/src/main/java/org/commcare/resources/model/ResourceTable.java @@ -942,7 +942,7 @@ public void initializeResources(CommCarePlatform platform, boolean isUpgrade) th setMissingResources(missingResources); } - private void attemptResourceInitialization(CommCarePlatform platform, boolean isUpgrade, + public void attemptResourceInitialization(CommCarePlatform platform, boolean isUpgrade, Resource r, Vector<Resource> missingResources) throws ResourceInitializationException { try { r.getInstaller().initialize(platform, isUpgrade); From 17ac4a4b39e147d4961d6aa6eb1857c59bcc7a80 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Tue, 9 Sep 2025 09:09:11 +0530 Subject: [PATCH 390/402] check for empty values --- src/main/java/org/commcare/xml/ProfileParser.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/commcare/xml/ProfileParser.java b/src/main/java/org/commcare/xml/ProfileParser.java index 6eed7d6975..cbc46802ae 100644 --- a/src/main/java/org/commcare/xml/ProfileParser.java +++ b/src/main/java/org/commcare/xml/ProfileParser.java @@ -3,6 +3,7 @@ */ package org.commcare.xml; +import org.commcare.cases.util.StringUtils; import org.commcare.resources.model.Resource; import org.commcare.resources.model.ResourceTable; import org.commcare.suite.model.AndroidPackageDependency; @@ -266,10 +267,10 @@ private Vector<Credential> parseCredentials() if (tag.equals(NAME_CREDENTIAL)) { String level = parser.getAttributeValue(null, ATTR_CREDENTIAL_LEVEL); String type = parser.getAttributeValue(null, ATTR_CREDENTIAL_TYPE); - if (level == null) { + if (StringUtils.isEmpty(level)) { throw new InvalidStructureException("No level defined for credential"); } - if (type == null) { + if (StringUtils.isEmpty(type)) { throw new InvalidStructureException("No type defined for credential"); } appCredentials.add(new Credential(level, type)); From 76da3a4eba2c03469448596f9d2beff823bbb410 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Wed, 10 Sep 2025 23:49:58 +0530 Subject: [PATCH 391/402] append info to crash messaging --- src/main/java/org/javarosa/core/services/Logger.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/javarosa/core/services/Logger.java b/src/main/java/org/javarosa/core/services/Logger.java index 8501fbbf7e..740f01e817 100644 --- a/src/main/java/org/javarosa/core/services/Logger.java +++ b/src/main/java/org/javarosa/core/services/Logger.java @@ -53,10 +53,15 @@ public static void log(String type, String message) { public static void exception(String info, Throwable e) { e.printStackTrace(); - log(LogTypes.TYPE_EXCEPTION, (info != null ? info + ": " : "") + WrappedException.printException(e)); + info = info != null ? info + ": " : ""; + log(LogTypes.TYPE_EXCEPTION, info + WrappedException.printException(e)); if (logger != null) { try { - logger.logException(e); + String message = e.getMessage(); + if (message == null) { + message = ""; + } + logger.logException(new Exception(info + message, e)); } catch (RuntimeException ex) { logger.panic(); } From 0bd1faefb0be0672ca58d6211f95247f09e8c159 Mon Sep 17 00:00:00 2001 From: Martin Riese <mriese@dimagi.com> Date: Tue, 9 Sep 2025 16:16:13 -0500 Subject: [PATCH 392/402] Add reference field to FixtureInitializationException --- .../commcare/core/process/CommCareInstanceInitializer.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java b/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java index 467708c978..eecbc1b620 100644 --- a/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java +++ b/src/main/java/org/commcare/core/process/CommCareInstanceInitializer.java @@ -295,10 +295,14 @@ public String getVersionString() { public static class FixtureInitializationException extends RuntimeException { + public final String reference; + public FixtureInitializationException(String lookupReference) { super(Localization.getWithDefault("lookup.table.missing.error", new String[]{lookupReference}, "Unable to find lookup table: " + lookupReference)); + + this.reference = lookupReference; } } } From a1a19db7685bdf9462b75da211bb94d79089f7f5 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna <avazirna@gmail.com> Date: Tue, 30 Sep 2025 15:08:48 +0200 Subject: [PATCH 393/402] Bump Retrofit to version 2.9.0 --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index b049e15561..7a15ece772 100644 --- a/build.gradle +++ b/build.gradle @@ -75,7 +75,7 @@ dependencies { implementation 'commons-cli:commons-cli:1.3.1' implementation 'joda-time:joda-time:2.12.1' implementation 'com.carrotsearch:hppc:0.9.1' - api 'com.squareup.retrofit2:retrofit:2.3.0' + api 'com.squareup.retrofit2:retrofit:2.9.0' api 'com.squareup.okhttp3:okhttp:4.11.0' implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'io.reactivex.rxjava2:rxjava:2.2.21' @@ -89,7 +89,7 @@ dependencies { cliImplementation 'commons-cli:commons-cli:1.3.1' cliImplementation 'joda-time:joda-time:2.9.4' cliImplementation 'io.reactivex.rxjava2:rxjava:2.1.1' - cliImplementation 'com.squareup.retrofit2:retrofit:2.3.0' + cliImplementation 'com.squareup.retrofit2:retrofit:2.9.0' cliImplementation 'com.squareup.okhttp3:okhttp:4.11.0' cliImplementation 'com.google.guava:guava:31.1-jre' cliImplementation 'com.datadoghq:dd-trace-api:1.10.0' @@ -102,7 +102,7 @@ dependencies { testImplementation 'org.json:json:20250517' testImplementation 'org.xerial:sqlite-jdbc:3.40.0.0' testImplementation 'joda-time:joda-time:2.9.4' - testImplementation 'com.squareup.retrofit2:retrofit:2.3.0' + testImplementation 'com.squareup.retrofit2:retrofit:2.9.0' testImplementation 'com.squareup.okhttp3:okhttp:4.11.0' testImplementation 'com.google.guava:guava:31.1-jre' @@ -113,7 +113,7 @@ dependencies { ccapiImplementation 'joda-time:joda-time:2.9.4' ccapiImplementation 'com.carrotsearch:hppc:0.9.1' ccapiImplementation 'io.reactivex.rxjava2:rxjava:2.1.1' - ccapiImplementation "com.squareup.retrofit2:retrofit:2.3.0" + ccapiImplementation "com.squareup.retrofit2:retrofit:2.9.0" ccapiImplementation 'com.squareup.okhttp3:okhttp:4.11.0' ccapiImplementation 'com.google.guava:guava:31.1-jre' ccapiImplementation 'io.opentracing:opentracing-api:0.33.0' @@ -127,7 +127,7 @@ dependencies { translateImplementation 'joda-time:joda-time:2.9.4' translateImplementation 'org.json:json:20250517' translateImplementation 'io.reactivex.rxjava2:rxjava:2.1.1' - translateImplementation "com.squareup.retrofit2:retrofit:2.3.0" + translateImplementation "com.squareup.retrofit2:retrofit:2.9.0" translateImplementation 'com.squareup.okhttp3:okhttp:4.11.0' translateImplementation 'com.google.guava:guava:31.1-jre' From 89841dcfab6a7b241d9244a3ad6cd93ac29635a0 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Thu, 9 Oct 2025 17:09:06 +0200 Subject: [PATCH 394/402] Changes to facilititate more error response logging on mobile --- .../core/interfaces/HttpResponseProcessor.java | 2 +- .../core/interfaces/ResponseStreamAccessor.java | 2 ++ .../core/network/ModernHttpRequester.java | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/commcare/core/interfaces/HttpResponseProcessor.java b/src/main/java/org/commcare/core/interfaces/HttpResponseProcessor.java index c4e027accd..9c6392812b 100644 --- a/src/main/java/org/commcare/core/interfaces/HttpResponseProcessor.java +++ b/src/main/java/org/commcare/core/interfaces/HttpResponseProcessor.java @@ -20,7 +20,7 @@ public interface HttpResponseProcessor { * Can represent authentication issues, data parity issues between client * and server, among other things */ - void processClientError(int responseCode); + void processClientError(int responseCode, ResponseStreamAccessor streamAccessor); /** * Http response was in the 500s diff --git a/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java b/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java index d628e62f83..0f6e346355 100644 --- a/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java +++ b/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java @@ -5,5 +5,7 @@ public interface ResponseStreamAccessor { InputStream getResponseStream() throws IOException; + + InputStream getErrorResponseStream(); String getApiVersion(); } diff --git a/src/main/java/org/commcare/core/network/ModernHttpRequester.java b/src/main/java/org/commcare/core/network/ModernHttpRequester.java index 6ea3104e7f..6cf886442f 100644 --- a/src/main/java/org/commcare/core/network/ModernHttpRequester.java +++ b/src/main/java/org/commcare/core/network/ModernHttpRequester.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -99,6 +100,11 @@ public InputStream getResponseStream() throws IOException { return requester.getResponseStream(response); } + @Override + public InputStream getErrorResponseStream() { + return requester.getErrorResponseStream(response); + } + @Override public String getApiVersion() { return requester.getApiVersion(); @@ -175,7 +181,7 @@ public static void processResponse(HttpResponseProcessor responseProcessor, StreamsUtil.closeStream(responseStream); } } else if (responseCode >= 400 && responseCode < 500) { - responseProcessor.processClientError(responseCode); + responseProcessor.processClientError(responseCode, streamAccessor); } else if (responseCode >= 500 && responseCode < 600) { responseProcessor.processServerError(responseCode); } else { @@ -198,6 +204,14 @@ public InputStream getResponseStream(Response<ResponseBody> response) throws IOE return cache.retrieveCache(); } + @Nullable + public InputStream getErrorResponseStream(Response<ResponseBody> response) { + if (response.errorBody() != null) { + return response.errorBody().byteStream(); + } + return null; + } + public String getApiVersion() { return response != null ? response.headers().get("x-api-current-version") : null; } From 36d172699825c3d048ca6609a0cb7b55686cb456 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Mon, 13 Oct 2025 12:23:49 +0200 Subject: [PATCH 395/402] close stream in parent caller instead --- .../core/interfaces/HttpResponseProcessor.java | 2 +- .../core/interfaces/ResponseStreamAccessor.java | 3 +++ .../commcare/core/network/ModernHttpRequester.java | 12 +++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/commcare/core/interfaces/HttpResponseProcessor.java b/src/main/java/org/commcare/core/interfaces/HttpResponseProcessor.java index 9c6392812b..11ac9b420b 100644 --- a/src/main/java/org/commcare/core/interfaces/HttpResponseProcessor.java +++ b/src/main/java/org/commcare/core/interfaces/HttpResponseProcessor.java @@ -20,7 +20,7 @@ public interface HttpResponseProcessor { * Can represent authentication issues, data parity issues between client * and server, among other things */ - void processClientError(int responseCode, ResponseStreamAccessor streamAccessor); + void processClientError(int responseCode, InputStream errorStream); /** * Http response was in the 500s diff --git a/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java b/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java index 0f6e346355..898258e094 100644 --- a/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java +++ b/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java @@ -3,9 +3,12 @@ import java.io.IOException; import java.io.InputStream; +import javax.annotation.Nullable; + public interface ResponseStreamAccessor { InputStream getResponseStream() throws IOException; + @Nullable InputStream getErrorResponseStream(); String getApiVersion(); } diff --git a/src/main/java/org/commcare/core/network/ModernHttpRequester.java b/src/main/java/org/commcare/core/network/ModernHttpRequester.java index 6cf886442f..d4fb423074 100644 --- a/src/main/java/org/commcare/core/network/ModernHttpRequester.java +++ b/src/main/java/org/commcare/core/network/ModernHttpRequester.java @@ -8,6 +8,7 @@ import org.commcare.core.network.bitcache.BitCacheFactory; import org.commcare.util.NetworkStatus; import org.javarosa.core.io.StreamsUtil; +import org.javarosa.core.services.Logger; import java.io.IOException; import java.io.InputStream; @@ -100,6 +101,7 @@ public InputStream getResponseStream() throws IOException { return requester.getResponseStream(response); } + @Nullable @Override public InputStream getErrorResponseStream() { return requester.getErrorResponseStream(response); @@ -181,7 +183,15 @@ public static void processResponse(HttpResponseProcessor responseProcessor, StreamsUtil.closeStream(responseStream); } } else if (responseCode >= 400 && responseCode < 500) { - responseProcessor.processClientError(responseCode, streamAccessor); + InputStream errorStream = null; + try { + errorStream = streamAccessor.getErrorResponseStream(); + responseProcessor.processClientError(responseCode, errorStream); + } catch (Exception e) { + Logger.exception("Exception during network error processing", e); + } finally { + StreamsUtil.closeStream(errorStream); + } } else if (responseCode >= 500 && responseCode < 600) { responseProcessor.processServerError(responseCode); } else { From 42e31e03157fc88f7248aa24d2eb6f15089475ac Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Mon, 13 Oct 2025 12:28:31 +0200 Subject: [PATCH 396/402] cache error stream as well --- .../core/interfaces/ResponseStreamAccessor.java | 2 +- .../commcare/core/network/ModernHttpRequester.java | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java b/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java index 898258e094..e61ad466c4 100644 --- a/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java +++ b/src/main/java/org/commcare/core/interfaces/ResponseStreamAccessor.java @@ -9,6 +9,6 @@ public interface ResponseStreamAccessor { InputStream getResponseStream() throws IOException; @Nullable - InputStream getErrorResponseStream(); + InputStream getErrorResponseStream() throws IOException; String getApiVersion(); } diff --git a/src/main/java/org/commcare/core/network/ModernHttpRequester.java b/src/main/java/org/commcare/core/network/ModernHttpRequester.java index d4fb423074..92e5b3c46b 100644 --- a/src/main/java/org/commcare/core/network/ModernHttpRequester.java +++ b/src/main/java/org/commcare/core/network/ModernHttpRequester.java @@ -103,7 +103,7 @@ public InputStream getResponseStream() throws IOException { @Nullable @Override - public InputStream getErrorResponseStream() { + public InputStream getErrorResponseStream() throws IOException { return requester.getErrorResponseStream(response); } @@ -207,6 +207,11 @@ public static void processResponse(HttpResponseProcessor responseProcessor, public InputStream getResponseStream(Response<ResponseBody> response) throws IOException { InputStream inputStream = response.body().byteStream(); + return cacheResponse(inputStream, response); + } + + private InputStream cacheResponse(InputStream inputStream, Response<ResponseBody> response) + throws IOException { BitCache cache = BitCacheFactory.getCache(cacheDirSetup, getContentLength(response)); cache.initializeCache(); OutputStream cacheOut = cache.getCacheStream(); @@ -215,9 +220,9 @@ public InputStream getResponseStream(Response<ResponseBody> response) throws IOE } @Nullable - public InputStream getErrorResponseStream(Response<ResponseBody> response) { + public InputStream getErrorResponseStream(Response<ResponseBody> response) throws IOException { if (response.errorBody() != null) { - return response.errorBody().byteStream(); + return cacheResponse( response.errorBody().byteStream(), response); } return null; } From dd8437a43ffb66e421b6916e2648cafe2bd54eee Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Thu, 23 Oct 2025 16:04:25 +0530 Subject: [PATCH 397/402] removes duplicate entity skip change --- src/cli/java/org/commcare/util/screen/EntityScreen.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/cli/java/org/commcare/util/screen/EntityScreen.java b/src/cli/java/org/commcare/util/screen/EntityScreen.java index 2c12938828..4ab0c989d8 100755 --- a/src/cli/java/org/commcare/util/screen/EntityScreen.java +++ b/src/cli/java/org/commcare/util/screen/EntityScreen.java @@ -189,15 +189,6 @@ public boolean initReferences(SessionWrapper session) throws CommCareSessionExce } } - // if readyToSkip, entity screen will not be displayed. We don't need to init the subscreen - if (!readyToSkip) { - // if isDetailScreen or needsFullInit is not set, - // sub screen is needed to handle actions but we can skip eval refs - Vector<TreeReference> entityListReferences = - !needsFullInit || isDetailScreen() ? new Vector<>() : references; - mCurrentScreen = new EntityListSubscreen(mShortDetail, entityListReferences, evalContext, - handleCaseIndex, entityScreenContext); - } initialized = true; return true; } From a349e4e858e8670ccee78b7d10fb55671549cc02 Mon Sep 17 00:00:00 2001 From: Jonathan Jackson <jjackson@dimagi.com> Date: Fri, 31 Oct 2025 13:00:52 -0600 Subject: [PATCH 398/402] Fix CLI hanging on exceptions Add try-catch block around cliCommand.handle() to ensure the CLI exits cleanly with error code -1 when exceptions occur, rather than hanging indefinitely. This improves the development experience when using CLI tools like 'validate' for CCZ validation, allowing for faster iteration. --- src/cli/java/org/commcare/util/cli/CliMain.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/cli/java/org/commcare/util/cli/CliMain.java b/src/cli/java/org/commcare/util/cli/CliMain.java index d2e4605aa8..752bc8a2db 100644 --- a/src/cli/java/org/commcare/util/cli/CliMain.java +++ b/src/cli/java/org/commcare/util/cli/CliMain.java @@ -61,7 +61,13 @@ public static void main(String[] args) { return; } - cliCommand.handle(); + try { + cliCommand.handle(); + } catch (Exception e) { + System.err.println("\nValidation failed with error:"); + e.printStackTrace(); + System.exit(-1); + } } private static CliCommand getCliCommand(String commandName) throws CliCommandNotFound { From 273342c59479114f831bea0185ee5c2d82025a4b Mon Sep 17 00:00:00 2001 From: Shubham Goyal <sgoyal@dimagi.com> Date: Sat, 1 Nov 2025 10:12:35 +0530 Subject: [PATCH 399/402] Apply suggestion from @shubham1g5 --- src/cli/java/org/commcare/util/cli/CliMain.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/java/org/commcare/util/cli/CliMain.java b/src/cli/java/org/commcare/util/cli/CliMain.java index 752bc8a2db..50d1758430 100644 --- a/src/cli/java/org/commcare/util/cli/CliMain.java +++ b/src/cli/java/org/commcare/util/cli/CliMain.java @@ -64,7 +64,7 @@ public static void main(String[] args) { try { cliCommand.handle(); } catch (Exception e) { - System.err.println("\nValidation failed with error:"); + System.err.println("\Command failed with error:"); e.printStackTrace(); System.exit(-1); } From 8a452d02b6db5c67aa5fe6a71b66b2c8fea9c8a3 Mon Sep 17 00:00:00 2001 From: Shubham Goyal <sgoyal@dimagi.com> Date: Sat, 1 Nov 2025 10:15:28 +0530 Subject: [PATCH 400/402] Correct escape char --- src/cli/java/org/commcare/util/cli/CliMain.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/java/org/commcare/util/cli/CliMain.java b/src/cli/java/org/commcare/util/cli/CliMain.java index 50d1758430..8ad51ae546 100644 --- a/src/cli/java/org/commcare/util/cli/CliMain.java +++ b/src/cli/java/org/commcare/util/cli/CliMain.java @@ -64,7 +64,7 @@ public static void main(String[] args) { try { cliCommand.handle(); } catch (Exception e) { - System.err.println("\Command failed with error:"); + System.err.println("\nCommand failed with error:"); e.printStackTrace(); System.exit(-1); } From 1d3c02c9070a4c5c6591d649eea98cfd2395880b Mon Sep 17 00:00:00 2001 From: Shubham Goyal <shubham1g5@gmail.com> Date: Tue, 2 Dec 2025 22:41:40 +0530 Subject: [PATCH 401/402] adds argument validations for auth classes --- src/main/java/org/commcare/core/network/AuthInfo.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/org/commcare/core/network/AuthInfo.java b/src/main/java/org/commcare/core/network/AuthInfo.java index e60cdfe8c7..f2fbe590f2 100644 --- a/src/main/java/org/commcare/core/network/AuthInfo.java +++ b/src/main/java/org/commcare/core/network/AuthInfo.java @@ -1,5 +1,7 @@ package org.commcare.core.network; +import org.commcare.cases.util.StringUtils; + /** * Created by amstone326 on 5/8/18. */ @@ -22,6 +24,12 @@ public ProvidedAuth(String username, String password) { } public ProvidedAuth(String username, String password, boolean wrapDomain) { + if (StringUtils.isEmpty(username)) { + throw new IllegalArgumentException("ProvidedAuth requires a non-empty username"); + } + if (StringUtils.isEmpty(password)) { + throw new IllegalArgumentException("ProvidedAuth requires a non-empty password"); + } this.username = username; this.password = password; this.wrapDomain = wrapDomain; @@ -35,6 +43,9 @@ public static class CurrentAuth extends AuthInfo { public static class TokenAuth extends AuthInfo { public TokenAuth(String token) { + if (StringUtils.isEmpty(token)) { + throw new IllegalArgumentException("TokenAuth requires a non-empty token"); + } bearerToken = token; } } From b660fd8e766bebf4bcf38b0ff8eed477469cd8c0 Mon Sep 17 00:00:00 2001 From: David Viggiano <dave.viggiano@gmail.com> Date: Wed, 10 Dec 2025 11:26:05 -0500 Subject: [PATCH 402/402] Added createPointList helper method to GeoPointUtils. Using the new helper in PolygonUtils.createPolygon. Moved validateCoordinates from PolygonUtils to GeoPointUtils. --- .../core/model/utils/GeoPointUtils.java | 43 +++++++++++++++++++ .../core/model/utils/PolygonUtils.java | 31 +------------ .../expr/XPathClosestPointOnPolygonFunc.java | 3 +- .../expr/XPathIsPointInsidePolygonFunc.java | 3 +- 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/javarosa/core/model/utils/GeoPointUtils.java b/src/main/java/org/javarosa/core/model/utils/GeoPointUtils.java index a2bb0ae7e1..a69a718e06 100644 --- a/src/main/java/org/javarosa/core/model/utils/GeoPointUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/GeoPointUtils.java @@ -1,7 +1,11 @@ package org.javarosa.core.model.utils; +import org.gavaghan.geodesy.GlobalCoordinates; import org.javarosa.core.model.data.GeoPointData; +import java.util.ArrayList; +import java.util.List; + /** * Static utility methods for GeoPointData. * @@ -62,4 +66,43 @@ private static double arcHav(double x) { private static double havDistance(double lat1, double lat2, double dLng) { return hav(lat1 - lat2) + hav(dLng) * Math.cos(lat1) * Math.cos(lat2); } + + /** + * Checks if coordinates are within valid bounds for latitude and longitude. + * + * @param latitude Latitude in degrees + * @param longitude Longitude in degrees + * @throws IllegalArgumentException if values are outside geographic bounds + */ + public static void validateCoordinates(double latitude, double longitude) { + if ((latitude < -90.0 || latitude > 90.0) || (longitude < -180.0 || longitude > 180.0)) { + throw new IllegalArgumentException("Invalid coordinates"); + } + } + + /** + * Creates a point list from a flat list of lat/lon strings. + * + * @param latLongList Flat list of lat/lon values (e.g., [lat1, lon1, lat2, lon2, ...]) + * @return List of GlobalCoordinates representing the list of points + * @throws IllegalArgumentException if input is invalid (odd number of elements) + */ + public static List<GlobalCoordinates> createPointList(List<String> latLongList) throws IllegalArgumentException { + if (latLongList == null || latLongList.size() % 2 != 0) { + throw new IllegalArgumentException( + "Input must contain a list of lat/lng pairs, and must be even-sized."); + } + + int numPoints = latLongList.size() / 2; + List<GlobalCoordinates> pointList = new ArrayList<>(); + + for (int i = 0; i < numPoints; i++) { + double latitude = Double.parseDouble(latLongList.get(i * 2)); + double longitude = Double.parseDouble(latLongList.get(i * 2 + 1)); + validateCoordinates(latitude, longitude); + pointList.add(new GlobalCoordinates(latitude, longitude)); + } + + return pointList; + } } diff --git a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java index 798663fe07..78163d6af1 100644 --- a/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java +++ b/src/main/java/org/javarosa/core/model/utils/PolygonUtils.java @@ -4,7 +4,6 @@ import org.gavaghan.geodesy.GeodeticCurve; import org.gavaghan.geodesy.GlobalCoordinates; -import java.util.ArrayList; import java.util.List; /** @@ -21,23 +20,10 @@ public class PolygonUtils { * @throws IllegalArgumentException if input is invalid or polygon is malformed */ public static List<GlobalCoordinates> createPolygon(List<String> latLongList) throws IllegalArgumentException { - if (latLongList == null || latLongList.size() < 6 || latLongList.size() % 2 != 0) { - throw new IllegalArgumentException( - "Input must contain at least three lat/lng pairs (six elements total), and must be even-sized."); - } - - int numPoints = latLongList.size() / 2; - List<GlobalCoordinates> polygon = new ArrayList<>(); - - for (int i = 0; i < numPoints; i++) { - double latitude = Double.parseDouble(latLongList.get(i * 2)); - double longitude = Double.parseDouble(latLongList.get(i * 2 + 1)); - validateCoordinates(latitude, longitude); - polygon.add(new GlobalCoordinates(latitude, longitude)); - } + List<GlobalCoordinates> polygon = GeoPointUtils.createPointList(latLongList); // Close polygon if not already closed - if (!polygon.get(0).equals(polygon.get(polygon.size() - 1))) { + if (polygon.size() > 2 && !polygon.get(0).equals(polygon.get(polygon.size() - 1))) { polygon.add(new GlobalCoordinates( polygon.get(0).getLatitude(), polygon.get(0).getLongitude())); @@ -50,19 +36,6 @@ public static List<GlobalCoordinates> createPolygon(List<String> latLongList) th return polygon; } - /** - * Checks if coordinates are within valid bounds for latitude and longitude. - * - * @param latitude Latitude in degrees - * @param longitude Longitude in degrees - * @throws IllegalArgumentException if values are outside geographic bounds - */ - public static void validateCoordinates(double latitude, double longitude) { - if ((latitude < -90.0 || latitude > 90.0) || (longitude < -180.0 || longitude > 180.0)) { - throw new IllegalArgumentException("Invalid polygon coordinates"); - } - } - /** * Computes the closest point on the polygon border to a given test point using geodesic projection. * diff --git a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java index 0564755a2f..a497c3614d 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathClosestPointOnPolygonFunc.java @@ -5,6 +5,7 @@ import org.javarosa.core.model.data.GeoPointData; import org.javarosa.core.model.data.UncastData; import org.javarosa.core.model.instance.DataInstance; +import org.javarosa.core.model.utils.GeoPointUtils; import org.javarosa.core.model.utils.PolygonUtils; import org.javarosa.xpath.XPathException; import org.javarosa.xpath.XPathTypeMismatchException; @@ -74,7 +75,7 @@ private static String closestPointToPolygon(Object from, Object to) { String[] coordinates = inputPolygon.split(" "); List<GlobalCoordinates> polygon = PolygonUtils.createPolygon(Arrays.asList(coordinates)); GeoPointData pointData = new GeoPointData().cast(new UncastData(inputPoint)); - PolygonUtils.validateCoordinates(pointData.getLatitude(), pointData.getLongitude()); + GeoPointUtils.validateCoordinates(pointData.getLatitude(), pointData.getLongitude()); GlobalCoordinates pointCoordinates = new GlobalCoordinates(pointData.getLatitude(), pointData.getLongitude()); return PolygonUtils.findClosestPoint(pointCoordinates, polygon).toString(); diff --git a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java index d9c39073dc..6de573bee1 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathIsPointInsidePolygonFunc.java @@ -5,6 +5,7 @@ import org.javarosa.core.model.data.GeoPointData; import org.javarosa.core.model.data.UncastData; import org.javarosa.core.model.instance.DataInstance; +import org.javarosa.core.model.utils.GeoPointUtils; import org.javarosa.core.model.utils.PolygonUtils; import org.javarosa.xpath.XPathException; import org.javarosa.xpath.XPathTypeMismatchException; @@ -69,7 +70,7 @@ private static boolean isPointWithinBoundary(Object from, Object to) { String[] coordinates = inputPolygon.split(" "); List<GlobalCoordinates> polygon = PolygonUtils.createPolygon(Arrays.asList(coordinates)); GeoPointData pointData = new GeoPointData().cast(new UncastData(inputPoint)); - PolygonUtils.validateCoordinates(pointData.getLatitude(), pointData.getLongitude()); + GeoPointUtils.validateCoordinates(pointData.getLatitude(), pointData.getLongitude()); GlobalCoordinates pointCoordinates = new GlobalCoordinates(pointData.getLatitude(), pointData.getLongitude()); return PolygonUtils.isPointInsideOrOnPolygon(pointCoordinates, polygon);