From 744e8a21023187d9590d3b0a9bfd5f2acbd6378a Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 8 Jul 2024 16:04:18 +0100 Subject: [PATCH] Add partial elements --- PLUGINS.md | 1 - .../model/instance/CsvExternalInstance.java | 3 +- .../core/model/instance/DataInstance.java | 30 +- .../model/instance/ExternalDataInstance.java | 26 +- .../core/model/instance/TreeElement.java | 22 ++ .../geojson/GeoJsonExternalInstance.java | 3 +- .../java/org/javarosa/test/TempFileUtils.java | 2 +- .../xform/parse/ExternalInstanceParser.java | 28 +- .../org/javarosa/xform/parse/XFormParser.java | 13 + .../org/javarosa/xform/util/XFormUtils.java | 6 +- .../javarosa/plugins/InstancePluginTest.java | 271 ++++++++++++++++++ 11 files changed, 370 insertions(+), 35 deletions(-) create mode 100644 src/test/java/org/javarosa/plugins/InstancePluginTest.java diff --git a/PLUGINS.md b/PLUGINS.md index 512d3bd22..f01b26ed7 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -26,6 +26,5 @@ Inspect external instances (their ID and parsed XML) after parsing or provide cu ### API - `ExternalInstanceParser#addFileInstanceParser` -- `ExternalInstanceparsser#addProcessor` The default `ExternalInstanceParser` can be overridden by creating an implementation of `ExternalInstanceParserFactory` and calling `XFormUtils.setExternalInstanceParserFactory` with it. \ No newline at end of file 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 70dd12ca5..0ec3da52c 100644 --- a/src/main/java/org/javarosa/core/model/instance/CsvExternalInstance.java +++ b/src/main/java/org/javarosa/core/model/instance/CsvExternalInstance.java @@ -6,6 +6,7 @@ import org.apache.commons.io.input.BOMInputStream; import org.javarosa.core.model.data.UncastData; import org.javarosa.xform.parse.ExternalInstanceParser; +import org.jetbrains.annotations.NotNull; import java.io.BufferedReader; import java.io.FileInputStream; @@ -16,7 +17,7 @@ public class CsvExternalInstance implements ExternalInstanceParser.FileInstanceParser { - public TreeElement parse(String instanceId, String path) throws IOException { + public TreeElement parse(@NotNull String instanceId, @NotNull String path) throws IOException { final TreeElement root = new TreeElement("root", 0); root.setInstanceName(instanceId); 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 3506724f9..49d6ce0e6 100644 --- a/src/main/java/org/javarosa/core/model/instance/DataInstance.java +++ b/src/main/java/org/javarosa/core/model/instance/DataInstance.java @@ -1,11 +1,5 @@ package org.javarosa.core.model.instance; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - import org.javarosa.core.model.IDataReference; import org.javarosa.core.services.storage.Persistable; import org.javarosa.core.util.externalizable.DeserializationException; @@ -13,6 +7,12 @@ import org.javarosa.core.util.externalizable.ExtWrapNullable; import org.javarosa.core.util.externalizable.PrototypeFactory; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + /** * A data instance represents a tree structure of abstract tree * elements which can be accessed and read with tree references. It is @@ -81,6 +81,10 @@ public T resolveReference(TreeReference ref) { AbstractTreeElement node = getBase(); T result = null; for (int i = 0; i < ref.size(); i++) { + if (node instanceof TreeElement && ((TreeElement) node).isPartial()) { + throw new PartialElementEncounteredException(); + } + String name = ref.getName(i); int mult = ref.getMultiplicity(i); @@ -299,4 +303,18 @@ public void setID(int recordid) { public abstract void initialize(InstanceInitializationFactory initializer, String instanceId); + public void replacePartialElements(List elements) { + for (TreeElement element : elements) { + TreeElement root = (TreeElement) getRoot(); + TreeElement matchingChild = root.getChild(element.getName(), element.getMultiplicity()); + + if (matchingChild != null) { + matchingChild.populatePartial(element); + } + } + } + + public static class PartialElementEncounteredException extends RuntimeException { + + } } \ No newline at end of file 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 6c6af0cae..bdc7338f9 100644 --- a/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java +++ b/src/main/java/org/javarosa/core/model/instance/ExternalDataInstance.java @@ -62,7 +62,7 @@ public static ExternalDataInstance build(String instanceSrc, String instanceId) throws IOException, UnfullfilledRequirementsException, XmlPullParserException, InvalidStructureException { TreeElement root; try { - root = parseExternalInstance(instanceSrc, instanceId); + root = XFormUtils.getExternalInstance(ReferenceManager.instance(), instanceId, instanceSrc, true); // Avoid parse error for missing name and label refs if a select is built on an empty placeholder file if (!root.hasChildren()) { @@ -76,9 +76,20 @@ public static ExternalDataInstance build(String instanceSrc, String instanceId) return new ExternalDataInstance(root, instanceId, instanceSrc); } - private static TreeElement parseExternalInstance(String instanceSrc, String instanceId) - throws IOException, InvalidReferenceException, InvalidStructureException, XmlPullParserException, UnfullfilledRequirementsException { - return XFormUtils.getExternalInstance(ReferenceManager.instance(), instanceId, instanceSrc); + @Override + public AbstractTreeElement resolveReference(TreeReference ref) { + try { + return super.resolveReference(ref); + } catch (PartialElementEncounteredException e) { + try { + parseExternalFile(false); + } catch (InvalidReferenceException | InvalidStructureException | XmlPullParserException | + UnfullfilledRequirementsException | IOException exception) { + throw new RuntimeException(new DeserializationException("Unable to parse external instance: " + exception)); + } + + return resolveReference(ref); + } } @Override @@ -114,7 +125,7 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) super.readExternal(in, pf); path = ExtUtil.readString(in); try { - setRoot(parseExternalInstance(path, getInstanceId())); + parseExternalFile(true); } catch (InvalidReferenceException | InvalidStructureException | XmlPullParserException | UnfullfilledRequirementsException e) { throw new DeserializationException("Unable to parse external instance: " + e); @@ -126,4 +137,9 @@ public void writeExternal(DataOutputStream out) throws IOException { super.writeExternal(out); ExtUtil.write(out, path); } + + private void parseExternalFile(boolean partial) throws UnfullfilledRequirementsException, InvalidStructureException, XmlPullParserException, IOException, InvalidReferenceException { + String instanceId = getInstanceId(); + setRoot(XFormUtils.getExternalInstance(ReferenceManager.instance(), instanceId, path, partial)); + } } \ No newline at end of file diff --git a/src/main/java/org/javarosa/core/model/instance/TreeElement.java b/src/main/java/org/javarosa/core/model/instance/TreeElement.java index f73226662..6c62fbbeb 100644 --- a/src/main/java/org/javarosa/core/model/instance/TreeElement.java +++ b/src/main/java/org/javarosa/core/model/instance/TreeElement.java @@ -106,6 +106,7 @@ public class TreeElement implements Externalizable, AbstractTreeElement(0); } + public TreeElement(String name, int multiplicity, boolean isPartial) { + this(name, multiplicity); + this.isPartial = isPartial; + } + /** * Construct a TreeElement which represents an attribute with the provided * namespace and name. @@ -1141,4 +1147,20 @@ public String getNamespacePrefix() { public void setNamespacePrefix(String namespacePrefix) { this.namespacePrefix = namespacePrefix; } + + public boolean isPartial() { + return isPartial; + } + + public void populatePartial(TreeElement element) { + if (isPartial) { + children.clear(); + + for (int i = 0; i < element.getNumChildren(); i++) { + addChild(element.getChildAt(i)); + } + + isPartial = false; + } + } } 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 51f9c8cf0..20d84c99d 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 @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.xform.parse.ExternalInstanceParser; +import org.jetbrains.annotations.NotNull; import java.io.FileInputStream; import java.io.IOException; @@ -28,7 +29,7 @@ public class GeoJsonExternalInstance implements ExternalInstanceParser.FileInstanceParser { - public TreeElement parse(String instanceId, String path) throws IOException { + public TreeElement parse(@NotNull String instanceId, @NotNull String path) throws IOException { final TreeElement root = new TreeElement("root", 0); root.setInstanceName(instanceId); diff --git a/src/main/java/org/javarosa/test/TempFileUtils.java b/src/main/java/org/javarosa/test/TempFileUtils.java index a1f52f791..9afa49d3a 100644 --- a/src/main/java/org/javarosa/test/TempFileUtils.java +++ b/src/main/java/org/javarosa/test/TempFileUtils.java @@ -13,7 +13,7 @@ static File createTempDir(String name) { return subDir; } - static File createTempFile(String prefix, String suffix) { + public static File createTempFile(String prefix, String suffix) { return createTempFile(null, prefix, suffix); } diff --git a/src/main/java/org/javarosa/xform/parse/ExternalInstanceParser.java b/src/main/java/org/javarosa/xform/parse/ExternalInstanceParser.java index 9b70ba5d1..9c8323b80 100644 --- a/src/main/java/org/javarosa/xform/parse/ExternalInstanceParser.java +++ b/src/main/java/org/javarosa/xform/parse/ExternalInstanceParser.java @@ -12,7 +12,6 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -22,13 +21,12 @@ public class ExternalInstanceParser { - private List externalDataInstanceProcessors = new ArrayList<>(); private List fileInstanceParsers = asList( new CsvExternalInstance(), new GeoJsonExternalInstance() ); - public TreeElement parse(ReferenceManager referenceManager, String instanceId, String instanceSrc) throws IOException, UnfullfilledRequirementsException, InvalidStructureException, XmlPullParserException, InvalidReferenceException { + public TreeElement parse(ReferenceManager referenceManager, String instanceId, String instanceSrc, boolean partial) throws IOException, UnfullfilledRequirementsException, InvalidStructureException, XmlPullParserException, InvalidReferenceException { String path = getPath(referenceManager, instanceSrc); Optional fileParser = fileInstanceParsers.stream() @@ -37,20 +35,15 @@ public TreeElement parse(ReferenceManager referenceManager, String instanceId, S TreeElement root; if (fileParser.isPresent()) { - root = fileParser.get().parse(instanceId, path); + root = fileParser.get().parse(instanceId, path, partial); } else { root = XmlExternalInstance.parse(instanceId, path); } - - for (ExternalDataInstanceProcessor processor : externalDataInstanceProcessors) { - processor.processInstance(instanceId, root); - } - return root; } - public void addProcessor(Processor processor) { - externalDataInstanceProcessors.add((ExternalDataInstanceProcessor) processor); + public TreeElement parse(ReferenceManager referenceManager, String instanceId, String instanceSrc) throws IOException, UnfullfilledRequirementsException, InvalidStructureException, XmlPullParserException, InvalidReferenceException { + return parse(referenceManager, instanceId, instanceSrc, false); } /** @@ -75,16 +68,13 @@ private static String getPath(ReferenceManager referenceManager, String srcLocat return uri.startsWith("//") /* todo why is this? */ ? uri.substring(1) : uri; } - public interface Processor { - - } + public interface FileInstanceParser { + TreeElement parse(@NotNull String instanceId, @NotNull String path) throws IOException; - public interface ExternalDataInstanceProcessor extends ExternalInstanceParser.Processor { - void processInstance(@NotNull String id, @NotNull TreeElement root); - } + default TreeElement parse(@NotNull String instanceId, @NotNull String path, boolean partial) throws IOException { + return parse(instanceId, path); + } - public interface FileInstanceParser { - TreeElement parse(String instanceId, String path) throws IOException; boolean isSupported(String instanceId, String instanceSrc); } } diff --git a/src/main/java/org/javarosa/xform/parse/XFormParser.java b/src/main/java/org/javarosa/xform/parse/XFormParser.java index b4504a672..bf6146e29 100644 --- a/src/main/java/org/javarosa/xform/parse/XFormParser.java +++ b/src/main/java/org/javarosa/xform/parse/XFormParser.java @@ -184,6 +184,7 @@ public class XFormParser implements IXFormParserFunctions { private final List modelAttributeProcessors = new ArrayList<>(); private final List questionProcessors = new ArrayList<>(); private final List xpathProcessors = new ArrayList<>(); + private final List externalDataInstanceProcessors = new ArrayList<>();; public static final List tempXPathProcessors = new ArrayList<>(); @@ -459,6 +460,10 @@ public void addProcessor(Processor processor) { if (processor instanceof XPathProcessor) { xpathProcessors.add((XPathProcessor) processor); } + + if (processor instanceof ExternalDataInstanceProcessor) { + externalDataInstanceProcessors.add((ExternalDataInstanceProcessor) processor); + } } public void addBindAttributeProcessor(BindAttributeProcessor bindAttributeProcessor) { @@ -577,6 +582,10 @@ private void parseDoc(String formXmlSrc, Map namespacePrefixesBy ExternalDataInstance externalDataInstance; try { externalDataInstance = ExternalDataInstance.build(instanceSrc, instanceId); + for (ExternalDataInstanceProcessor processor : externalDataInstanceProcessors) { + processor.processInstance(externalDataInstance); + } + } catch (IOException | UnfullfilledRequirementsException | InvalidStructureException | XmlPullParserException e) { String msg = "Unable to parse external secondary instance"; logger.error(msg, e); @@ -2488,6 +2497,10 @@ public interface QuestionProcessor extends Processor { void processQuestion(@NotNull QuestionDef question); } + public interface ExternalDataInstanceProcessor extends Processor { + void processInstance(@NotNull ExternalDataInstance instance); + } + public static class ParseException extends Exception { public ParseException() { diff --git a/src/main/java/org/javarosa/xform/util/XFormUtils.java b/src/main/java/org/javarosa/xform/util/XFormUtils.java index bba3e78da..d35032088 100644 --- a/src/main/java/org/javarosa/xform/util/XFormUtils.java +++ b/src/main/java/org/javarosa/xform/util/XFormUtils.java @@ -179,8 +179,12 @@ public static FormDef getFormFromSerializedResource(String resource) { return returnForm; } + public static TreeElement getExternalInstance(ReferenceManager referenceManager, String id, String instanceSrc, boolean partial) throws UnfullfilledRequirementsException, InvalidStructureException, XmlPullParserException, IOException, InvalidReferenceException { + return externalInstanceParserFactory.getExternalInstanceParser().parse(referenceManager, id, instanceSrc, partial); + } + public static TreeElement getExternalInstance(ReferenceManager referenceManager, String id, String instanceSrc) throws UnfullfilledRequirementsException, InvalidStructureException, XmlPullParserException, IOException, InvalidReferenceException { - return externalInstanceParserFactory.getExternalInstanceParser().parse(referenceManager, id, instanceSrc); + return getExternalInstance(referenceManager, id, instanceSrc, false); } /////Parser Attribute warning stuff diff --git a/src/test/java/org/javarosa/plugins/InstancePluginTest.java b/src/test/java/org/javarosa/plugins/InstancePluginTest.java new file mode 100644 index 000000000..50534e8bd --- /dev/null +++ b/src/test/java/org/javarosa/plugins/InstancePluginTest.java @@ -0,0 +1,271 @@ +package org.javarosa.plugins; + +import kotlin.Pair; +import org.javarosa.core.model.SelectChoice; +import org.javarosa.core.model.data.StringData; +import org.javarosa.core.model.instance.DataInstance; +import org.javarosa.core.model.instance.TreeElement; +import org.javarosa.test.Scenario; +import org.javarosa.test.TempFileUtils; +import org.javarosa.xform.parse.ExternalInstanceParser; +import org.javarosa.xform.parse.ExternalInstanceParserFactory; +import org.javarosa.xform.parse.XFormParser; +import org.javarosa.xform.util.XFormUtils; +import org.jetbrains.annotations.NotNull; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.javarosa.core.reference.ReferenceManagerTestUtils.setUpSimpleReferenceManager; +import static org.javarosa.test.BindBuilderXFormsElement.bind; +import static org.javarosa.test.XFormsElement.body; +import static org.javarosa.test.XFormsElement.head; +import static org.javarosa.test.XFormsElement.html; +import static org.javarosa.test.XFormsElement.mainInstance; +import static org.javarosa.test.XFormsElement.model; +import static org.javarosa.test.XFormsElement.select1Dynamic; +import static org.javarosa.test.XFormsElement.t; +import static org.javarosa.test.XFormsElement.title; + +public class InstancePluginTest { + + private final SwitchableExternalInstanceParserFactory externalInstanceParserFactory = new SwitchableExternalInstanceParserFactory(); + + @Before + public void setup() { + XFormUtils.setExternalInstanceParserFactory(externalInstanceParserFactory); + } + + @After + public void teardown() { + XFormUtils.setExternalInstanceParserFactory(ExternalInstanceParser::new); + } + + @Test + public void supportsPartialElements() throws IOException, XFormParser.ParseException { + externalInstanceParserFactory.setFileInstanceParser(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 supportsReplacingPartialElements() throws IOException, XFormParser.ParseException { + externalInstanceParserFactory.setFileInstanceParser(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"); + + TreeElement item = new TreeElement("item", 0); + TreeElement value = new TreeElement("value"); + TreeElement label = new TreeElement("label"); + value.setValue(new StringData("0")); + label.setValue(new StringData("Item 0")); + item.addChild(value); + item.addChild(label); + fakeInstance.replacePartialElements(asList(item)); + + TreeElement 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"))); + TreeElement secondItem = (TreeElement) fakeInstance.getRoot().getChild("item", 1); + assertThat(secondItem.isPartial(), equalTo(true)); + assertThat(secondItem.getNumChildren(), equalTo(2)); + assertThat(secondItem.getChildAt(0).getName(), equalTo("value")); + assertThat(secondItem.getChildAt(0).getValue(), equalTo(null)); + assertThat(secondItem.getChildAt(1).getName(), equalTo("label")); + assertThat(secondItem.getChildAt(1).getValue(), equalTo(null)); + } + + @Test + public void replacePartialElements_DoesNotOverrideNonPartialElements() throws IOException, XFormParser.ParseException { + externalInstanceParserFactory.setFileInstanceParser(new FakeFileInstanceParser(asList( + new Pair<>("0", "Item 0") + ), false)); + + 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"); + + TreeElement item = new TreeElement("item", 0); + TreeElement value = new TreeElement("value"); + TreeElement label = new TreeElement("label"); + value.setValue(new StringData("1")); + label.setValue(new StringData("Item 1")); + item.addChild(value); + item.addChild(label); + fakeInstance.replacePartialElements(asList(item)); + + TreeElement 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"))); + } + + private static class SwitchableExternalInstanceParserFactory implements ExternalInstanceParserFactory { + private ExternalInstanceParser.FileInstanceParser fileInstanceParser; + + @Override + public ExternalInstanceParser getExternalInstanceParser() { + ExternalInstanceParser externalInstanceParser = new ExternalInstanceParser(); + externalInstanceParser.addFileInstanceParser(fileInstanceParser); + return externalInstanceParser; + } + + public void setFileInstanceParser(ExternalInstanceParser.FileInstanceParser fileInstanceParser) { + this.fileInstanceParser = fileInstanceParser; + } + } + + public static class FakeFileInstanceParser implements ExternalInstanceParser.FileInstanceParser { + + private final List> items; + private final boolean partialParse; + + public FakeFileInstanceParser(List> items, boolean partialParse) { + this.items = items; + this.partialParse = partialParse; + } + + @Override + public TreeElement parse(@NotNull String instanceId, @NotNull String path) throws IOException { + return parse(instanceId, path, false); + } + + @Override + public TreeElement parse(@NotNull String instanceId, @NotNull String path, boolean partial) throws IOException { + boolean isPartial = partialParse && partial; + TreeElement root = new TreeElement("root", 0); + + for (int i = 0; i < items.size(); i++) { + TreeElement value = new TreeElement("value"); + TreeElement label = new TreeElement("label"); + + if (!isPartial) { + value.setValue(new StringData(items.get(i).getFirst())); + label.setValue(new StringData(items.get(i).getSecond())); + } + + TreeElement item = new TreeElement("item", i, isPartial); + item.addChild(value); + item.addChild(label); + + root.addChild(item); + } + + return root; + } + + @Override + public boolean isSupported(String instanceId, String instanceSrc) { + return instanceSrc.endsWith(".fake"); + } + } +}