From 62355e401a6f7acfbd3d5004c7931325a43ca8ac Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 16 Jul 2024 09:33:26 +0100 Subject: [PATCH 1/3] Add way to provide custom instance parsing that's not backed by a file --- PLUGINS.md | 1 + .../model/instance/CsvExternalInstance.java | 2 +- .../geojson/GeoJsonExternalInstance.java | 2 +- .../xform/parse/ExternalInstanceParser.java | 52 +++++++++++++++---- .../javarosa/plugins/InstancePluginTest.java | 2 +- 5 files changed, 46 insertions(+), 13 deletions(-) diff --git a/PLUGINS.md b/PLUGINS.md index 12013a28a..b16075ae7 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -25,6 +25,7 @@ Inspect the `FormEntryModel` after finalization (or "post processing") and attac Inspect external instances (their ID and parsed XML) after parsing or provide custom parsers for specific instances or file types. ### API +- `ExternalInstanceParser#addInstanceProvider` - `ExternalInstanceParser#addFileInstanceParser` The default `ExternalInstanceParser` can be overridden by creating an implementation of `ExternalInstanceParserFactory` and calling `XFormUtils.setExternalInstanceParserFactory` with it. diff --git a/src/main/java/org/javarosa/core/model/instance/CsvExternalInstance.java b/src/main/java/org/javarosa/core/model/instance/CsvExternalInstance.java index 0ec3da52c..4c51ab4a3 100644 --- a/src/main/java/org/javarosa/core/model/instance/CsvExternalInstance.java +++ b/src/main/java/org/javarosa/core/model/instance/CsvExternalInstance.java @@ -46,7 +46,7 @@ public TreeElement parse(@NotNull String instanceId, @NotNull String path) throw } @Override - public boolean isSupported(String instanceId, String instanceSrc) { + public boolean isSupported(@NotNull String instanceId, @NotNull String instanceSrc) { return instanceSrc.contains("file-csv"); } diff --git a/src/main/java/org/javarosa/core/model/instance/geojson/GeoJsonExternalInstance.java b/src/main/java/org/javarosa/core/model/instance/geojson/GeoJsonExternalInstance.java index 20d84c99d..5ba8bfeb8 100644 --- a/src/main/java/org/javarosa/core/model/instance/geojson/GeoJsonExternalInstance.java +++ b/src/main/java/org/javarosa/core/model/instance/geojson/GeoJsonExternalInstance.java @@ -74,7 +74,7 @@ public TreeElement parse(@NotNull String instanceId, @NotNull String path) throw } @Override - public boolean isSupported(String instanceId, String instanceSrc) { + public boolean isSupported(@NotNull String instanceId, @NotNull String instanceSrc) { return instanceSrc.endsWith("geojson"); } } diff --git a/src/main/java/org/javarosa/xform/parse/ExternalInstanceParser.java b/src/main/java/org/javarosa/xform/parse/ExternalInstanceParser.java index 9c8323b80..3091d669d 100644 --- a/src/main/java/org/javarosa/xform/parse/ExternalInstanceParser.java +++ b/src/main/java/org/javarosa/xform/parse/ExternalInstanceParser.java @@ -18,6 +18,7 @@ import java.util.stream.Stream; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; public class ExternalInstanceParser { @@ -26,20 +27,30 @@ public class ExternalInstanceParser { new GeoJsonExternalInstance() ); - public TreeElement parse(ReferenceManager referenceManager, String instanceId, String instanceSrc, boolean partial) throws IOException, UnfullfilledRequirementsException, InvalidStructureException, XmlPullParserException, InvalidReferenceException { - String path = getPath(referenceManager, instanceSrc); + private List instanceProviders = emptyList(); - Optional fileParser = fileInstanceParsers.stream() - .filter(fileInstanceParser -> fileInstanceParser.isSupported(instanceId, instanceSrc)) + public TreeElement parse(ReferenceManager referenceManager, String instanceId, String instanceSrc, boolean partial) throws IOException, UnfullfilledRequirementsException, InvalidStructureException, XmlPullParserException, InvalidReferenceException { + Optional instanceProvider = instanceProviders.stream() + .filter(parser -> parser.isSupported(instanceId, instanceSrc)) .findFirst(); - TreeElement root; - if (fileParser.isPresent()) { - root = fileParser.get().parse(instanceId, path, partial); + if (instanceProvider.isPresent()) { + return instanceProvider.get().get(instanceId, instanceSrc, partial); } else { - root = XmlExternalInstance.parse(instanceId, path); + String path = getPath(referenceManager, instanceSrc); + + Optional fileParser = fileInstanceParsers.stream() + .filter(parser -> parser.isSupported(instanceId, instanceSrc)) + .findFirst(); + + TreeElement root; + if (fileParser.isPresent()) { + root = fileParser.get().parse(instanceId, path, partial); + } else { + root = XmlExternalInstance.parse(instanceId, path); + } + return root; } - return root; } public TreeElement parse(ReferenceManager referenceManager, String instanceId, String instanceSrc) throws IOException, UnfullfilledRequirementsException, InvalidStructureException, XmlPullParserException, InvalidReferenceException { @@ -57,6 +68,17 @@ public void addFileInstanceParser(FileInstanceParser fileInstanceParser) { ).collect(Collectors.toList()); } + /** + * Adds {@link InstanceProvider} before others. The last added {@link InstanceProvider} will be checked + * (via {@link InstanceProvider#isSupported(String, String)}) first. + */ + public void addInstanceProvider(InstanceProvider instanceProvider) { + instanceProviders = Stream.concat( + Stream.of(instanceProvider), + instanceProviders.stream() + ).collect(Collectors.toList()); + } + /** * Returns the path of the URI at srcLocation. * @@ -75,6 +97,16 @@ default TreeElement parse(@NotNull String instanceId, @NotNull String path, bool return parse(instanceId, path); } - boolean isSupported(String instanceId, String instanceSrc); + boolean isSupported(@NotNull String instanceId, @NotNull String instanceSrc); + } + + public interface InstanceProvider { + TreeElement get(@NotNull String instanceId, @NotNull String path) throws IOException; + + default TreeElement get(@NotNull String instanceId, @NotNull String path, boolean partial) throws IOException { + return get(instanceId, path); + } + + boolean isSupported(@NotNull String instanceId, @NotNull String instanceSrc); } } diff --git a/src/test/java/org/javarosa/plugins/InstancePluginTest.java b/src/test/java/org/javarosa/plugins/InstancePluginTest.java index 50534e8bd..83800f225 100644 --- a/src/test/java/org/javarosa/plugins/InstancePluginTest.java +++ b/src/test/java/org/javarosa/plugins/InstancePluginTest.java @@ -264,7 +264,7 @@ public TreeElement parse(@NotNull String instanceId, @NotNull String path, boole } @Override - public boolean isSupported(String instanceId, String instanceSrc) { + public boolean isSupported(@NotNull String instanceId, @NotNull String instanceSrc) { return instanceSrc.endsWith(".fake"); } } From 7d6697a86c220f12da56b6e58f6770d4eddd2cfe Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 16 Jul 2024 09:39:04 +0100 Subject: [PATCH 2/3] Add test for InstanceProvider --- .../xform/parse/ExternalInstanceParser.java | 6 +- .../javarosa/plugins/InstancePluginTest.java | 97 +++++++++++++++++-- 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/javarosa/xform/parse/ExternalInstanceParser.java b/src/main/java/org/javarosa/xform/parse/ExternalInstanceParser.java index 3091d669d..4a620508c 100644 --- a/src/main/java/org/javarosa/xform/parse/ExternalInstanceParser.java +++ b/src/main/java/org/javarosa/xform/parse/ExternalInstanceParser.java @@ -101,10 +101,10 @@ default TreeElement parse(@NotNull String instanceId, @NotNull String path, bool } public interface InstanceProvider { - TreeElement get(@NotNull String instanceId, @NotNull String path) throws IOException; + TreeElement get(@NotNull String instanceId, @NotNull String instanceSrc) throws IOException; - default TreeElement get(@NotNull String instanceId, @NotNull String path, boolean partial) throws IOException { - return get(instanceId, path); + default TreeElement get(@NotNull String instanceId, @NotNull String instanceSrc, boolean partial) throws IOException { + return get(instanceId, instanceSrc); } boolean isSupported(@NotNull String instanceId, @NotNull String instanceSrc); diff --git a/src/test/java/org/javarosa/plugins/InstancePluginTest.java b/src/test/java/org/javarosa/plugins/InstancePluginTest.java index 83800f225..ed90c7f60 100644 --- a/src/test/java/org/javarosa/plugins/InstancePluginTest.java +++ b/src/test/java/org/javarosa/plugins/InstancePluginTest.java @@ -50,7 +50,61 @@ public void teardown() { } @Test - public void supportsPartialElements() throws IOException, XFormParser.ParseException { + public void instanceProvider_supportsPartialElements() throws IOException, XFormParser.ParseException { + externalInstanceParserFactory.setInstanceProvider(new FakeFileInstanceParser(asList( + new Pair<>("0", "Item 0"), + new Pair<>("1", "Item 1") + ), true)); + + File tempFile = TempFileUtils.createTempFile("fake-instance", "fake"); + setUpSimpleReferenceManager(tempFile, "file-csv", "file"); + + Scenario scenario = Scenario.init("Fake instance form", html( + head( + title("Fake instance form"), + model( + mainInstance( + t("data id=\"fake-instance-form\"", + t("question") + ) + ), + t("instance id=\"fake-instance\" src=\"jr://file-csv/fake-instance.fake\""), + bind("/data/question").type("string") + ) + ), + body( + select1Dynamic("/data/question", "instance('fake-instance')/root/item") + ) + ) + ); + + HashMap instances = scenario.getFormDef().getFormInstances(); + DataInstance fakeInstance = instances.get("fake-instance"); + assertThat(fakeInstance.getRoot().getNumChildren(), equalTo(2)); + + TreeElement firstItem = (TreeElement) fakeInstance.getRoot().getChild("item", 0); + assertThat(firstItem.isPartial(), equalTo(true)); + assertThat(firstItem.getNumChildren(), equalTo(2)); + assertThat(firstItem.getChildAt(0).getName(), equalTo("value")); + assertThat(firstItem.getChildAt(0).getValue(), equalTo(null)); + assertThat(firstItem.getChildAt(1).getName(), equalTo("label")); + assertThat(firstItem.getChildAt(1).getValue(), equalTo(null)); + + List selectChoices = scenario.choicesOf("/data/question"); + assertThat(selectChoices.size(), equalTo(2)); + + assertThat(selectChoices.get(0).getValue(), equalTo("0")); + firstItem = (TreeElement) fakeInstance.getRoot().getChild("item", 0); + assertThat(firstItem.isPartial(), equalTo(false)); + assertThat(firstItem.getNumChildren(), equalTo(2)); + assertThat(firstItem.getChildAt(0).getName(), equalTo("value")); + assertThat(firstItem.getChildAt(0).getValue(), equalTo(new StringData("0"))); + assertThat(firstItem.getChildAt(1).getName(), equalTo("label")); + assertThat(firstItem.getChildAt(1).getValue(), equalTo(new StringData("Item 0"))); + } + + @Test + public void fileInstanceParser_supportsPartialElements() throws IOException, XFormParser.ParseException { externalInstanceParserFactory.setFileInstanceParser(new FakeFileInstanceParser(asList( new Pair<>("0", "Item 0"), new Pair<>("1", "Item 1") @@ -211,20 +265,33 @@ public void replacePartialElements_DoesNotOverrideNonPartialElements() throws IO private static class SwitchableExternalInstanceParserFactory implements ExternalInstanceParserFactory { private ExternalInstanceParser.FileInstanceParser fileInstanceParser; + private ExternalInstanceParser.InstanceProvider instanceProvider; @Override public ExternalInstanceParser getExternalInstanceParser() { ExternalInstanceParser externalInstanceParser = new ExternalInstanceParser(); - externalInstanceParser.addFileInstanceParser(fileInstanceParser); + + if (fileInstanceParser != null) { + externalInstanceParser.addFileInstanceParser(fileInstanceParser); + } + + if (instanceProvider != null) { + externalInstanceParser.addInstanceProvider(instanceProvider); + } + return externalInstanceParser; } public void setFileInstanceParser(ExternalInstanceParser.FileInstanceParser fileInstanceParser) { this.fileInstanceParser = fileInstanceParser; } + + public void setInstanceProvider(ExternalInstanceParser.InstanceProvider instanceProvider) { + this.instanceProvider = instanceProvider; + } } - public static class FakeFileInstanceParser implements ExternalInstanceParser.FileInstanceParser { + public static class FakeFileInstanceParser implements ExternalInstanceParser.FileInstanceParser, ExternalInstanceParser.InstanceProvider { private final List> items; private final boolean partialParse; @@ -241,6 +308,25 @@ public TreeElement parse(@NotNull String instanceId, @NotNull String path) throw @Override public TreeElement parse(@NotNull String instanceId, @NotNull String path, boolean partial) throws IOException { + return createRoot(partial); + } + + @Override + public TreeElement get(@NotNull String instanceId, @NotNull String instanceSrc) throws IOException { + return get(instanceId, instanceSrc, false); + } + + @Override + public TreeElement get(@NotNull String instanceId, @NotNull String path, boolean partial) throws IOException { + return createRoot(partial); + } + + @Override + public boolean isSupported(@NotNull String instanceId, @NotNull String instanceSrc) { + return instanceSrc.endsWith(".fake"); + } + + private @NotNull TreeElement createRoot(boolean partial) { boolean isPartial = partialParse && partial; TreeElement root = new TreeElement("root", 0); @@ -262,10 +348,5 @@ public TreeElement parse(@NotNull String instanceId, @NotNull String path, boole return root; } - - @Override - public boolean isSupported(@NotNull String instanceId, @NotNull String instanceSrc) { - return instanceSrc.endsWith(".fake"); - } } } From 49d229be18d8cbea865a03bcc92f430adfe787e4 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 17 Jul 2024 14:19:10 +0100 Subject: [PATCH 3/3] Fix parseAttributes for expressions with an = in them --- src/main/java/org/javarosa/test/XFormsElement.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/javarosa/test/XFormsElement.java b/src/main/java/org/javarosa/test/XFormsElement.java index 7047c7731..f05ca57e3 100644 --- a/src/main/java/org/javarosa/test/XFormsElement.java +++ b/src/main/java/org/javarosa/test/XFormsElement.java @@ -44,7 +44,7 @@ static Map parseAttributes(String name) { Map attributes = new HashMap<>(); String[] words = name.split(" "); for (String word : asList(words).subList(1, words.length)) { - String[] parts = word.split("(?