diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/ResourceElement.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/ResourceElement.java new file mode 100644 index 000000000..a4ed46b8a --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/renderers/utils/ResourceElement.java @@ -0,0 +1,363 @@ +package org.hl7.fhir.r5.renderers.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BooleanSupplier; + +import org.hl7.fhir.r5.context.ContextUtilities; +import org.hl7.fhir.r5.elementmodel.Element; +import org.hl7.fhir.r5.model.Base; +import org.hl7.fhir.r5.model.Property; +import org.hl7.fhir.r5.model.Resource; + +/** + * This class is used to walk through the resources when rendering, whether + * the resource is a native resource or loaded by the element model + */ +public class ResourceElement { + + public enum ElementKind { + PrimitiveType, + DataType, + BackboneElement, + ContainedResource, + InlineResource, + BundleEntry, + IndependentResource + } + + private ContextUtilities context; + private ResourceElement parent; + private String name; // null at root + private int index; // -1 if not repeating + private ElementKind kind; + + private Base element; + private Element model; + + private List children; + + public ResourceElement(ContextUtilities context, Resource resource) { + this.context = context; + this.parent = null; + this.name = null; + this.index = -1; + this.kind = ElementKind.IndependentResource; + this.element = resource; + } + + public ResourceElement(ContextUtilities context, ResourceElement parent, String name, int index, ElementKind kind, Base element) { + this.context = context; + this.parent = parent; + this.name = name; + this.index = index; + this.kind = kind; + this.element = element; + } + + public ResourceElement(ContextUtilities context, Element resource) { + this.context = context; + this.parent = null; + this.name = null; + this.index = -1; + this.kind = ElementKind.IndependentResource; + this.model = resource; + } + + public ResourceElement(ContextUtilities context, ResourceElement parent, String name, int index, ElementKind kind, Element em) { + this.context = context; + this.parent = parent; + this.name = name; + this.index = index; + this.kind = kind; + this.model = em; + } + + public String path() { + if (parent == null) { + return fhirType(); + } else { + return parent.path()+"." + (index == -1 ? name : name+"["+index+"]"); + } + } + + public ElementKind kind() { + return kind; + } + + public String name() { + return name; + } + + public int index() { + return index; + } + + public String fhirType() { + if (kind == ElementKind.BackboneElement) { + return basePath(); + } else if (element != null) { + return element.fhirType(); + } else { + return model.fhirType(); + } + } + + private String basePath() { + if (parent == null || this.isResource()) { + return this.fhirType(); + } else { + return parent.basePath()+"."+name; + } + } + + public boolean isPrimitive() { + if (element != null) { + return element.isPrimitive(); + } else { + return model.isPrimitive(); + } + } + + public boolean hasPrimitiveValue() { + if (element != null) { + return element.hasPrimitiveValue(); + } else { + return model.hasPrimitiveValue(); + } + } + + public String primitiveValue() { + if (element != null) { + return element.primitiveValue(); + } else { + return model.primitiveValue(); + } + } + + public boolean isPrimitive(String name) { + ResourceElement child = child(name); + return child != null && child.isPrimitive(); + } + + public boolean hasPrimitiveValue(String name) { + ResourceElement child = child(name); + return child != null && child.hasPrimitiveValue(); + } + + public String primitiveValue(String name) { + ResourceElement child = child(name); + return child == null ? null : child.primitiveValue(); + } + + private void loadChildren() { + if (children == null) { + children = new ArrayList<>(); + if (element != null) { + loadElementChildren(); + } else { + loadModelChildren(); + } + } + } + + private void loadModelChildren() { + for (Element child : model.getChildren()) { + String name = child.getProperty().isChoice() ? child.getProperty().getName() : child.getName(); + int index = child.isList() ? child.getIndex() : -1; + ElementKind kind = determineModelKind(child); + children.add(new ResourceElement(context, this, name, index, kind, child)); + } + } + + private ElementKind determineModelKind(Element child) { + if (child.isPrimitive()) { + return ElementKind.PrimitiveType; + } else if (child.fhirType().contains("Backbone")) { + return ElementKind.BackboneElement; + } else if (child.getProperty().getContextUtils().isDatatype(child.fhirType())) { + return ElementKind.DataType; + } else if (!child.isResource()) { + return ElementKind.BackboneElement; + } else if (parent == null) { + return ElementKind.IndependentResource; + } else switch (child.getSpecial()) { + case BUNDLE_ENTRY: + return ElementKind.BundleEntry; + case BUNDLE_ISSUES: + return ElementKind.InlineResource; + case BUNDLE_OUTCOME: + return ElementKind.InlineResource; + case CONTAINED: + return ElementKind.ContainedResource; + case PARAMETER: + return ElementKind.InlineResource; + default: + return ElementKind.IndependentResource; + } + } + + private void loadElementChildren() { + for (Property p : element.children()) { + String name = p.getName(); + int i = 0; + for (Base v : p.getValues()) { + ElementKind kind = determineModelKind(p, v); + int index = p.isList() ? i : -1; + children.add(new ResourceElement(context, this, name, index, kind, v)); + i++; + } + } + } + + private ElementKind determineModelKind(Property p, Base v) { + if (v.isPrimitive()) { + return ElementKind.PrimitiveType; + } else if (context.isDatatype(v.fhirType())) { + return ElementKind.DataType; + } else if (!v.isResource()) { + return ElementKind.BackboneElement; + } else if (parent == null) { + return ElementKind.IndependentResource; + } else if ("Bundle.entry".equals(fhirType()) && "resource".equals(p.getName())) { + return ElementKind.BundleEntry; + } else if ("Bundle".equals(fhirType()) && "outcome".equals(p.getName())) { + return ElementKind.InlineResource; + } else if ("Bundle".equals(fhirType()) && "issues".equals(p.getName())) { + return ElementKind.InlineResource; + } else if (isResource() && "contained".equals(p.getName())) { + return ElementKind.ContainedResource; + } else { + return ElementKind.InlineResource; + } + } + + public List children() { + loadChildren(); + return children; + } + + public List children(String name) { + loadChildren(); + List list = new ArrayList(); + for (ResourceElement e : children) { + if (name.equals(e.name())) { + list.add(e); + } + } + return list; + } + + public ResourceElement child(String name) { + loadChildren(); + + ResourceElement res = null; + + for (ResourceElement e : children) { + if (name.equals(e.name()) || (name+"[x]").equals(e.name())) { + if (res == null) { + res = e; + } else { + throw new Error("Duplicated element '"+name+"' @ '"+path()+"'"); + } + } + } + return res; + } + + public boolean has(String name) { + for (ResourceElement e : children) { + if (name.equals(e.name())) { + return true; + } + } + return false; + } + + public ResourceElement resource() { + ResourceElement e = this.parent; + while (e != null && !e.isResource()) { + e = e.parent; + } + return e; + } + + public boolean isResource() { + if (element != null) { + return element.isResource(); + } else { + return model.isResource(); + } + } + + public boolean hasChildren() { + loadChildren(); + return !children.isEmpty(); + } + + public boolean hasExtension(String url) { + loadChildren(); + for (ResourceElement e : children) { + if ("Extension".equals(e.fhirType()) && url.equals(e.primitiveValue("url"))) { + return true; + } + } + return false; + } + + public ResourceElement extension(String url) { + ResourceElement res = null; + loadChildren(); + for (ResourceElement e : children) { + if ("Extension".equals(e.fhirType()) && url.equals(e.primitiveValue("url"))) { + if (res == null) { + res = e; + } else { + throw new Error("Duplicated extension '"+url+"' @ '"+path()+"'"); + } + } + } + return res; + } + + public ResourceElement extensionValue(String url) { + ResourceElement res = null; + loadChildren(); + for (ResourceElement e : children) { + if ("Extension".equals(e.fhirType()) && url.equals(e.primitiveValue("url"))) { + if (res == null) { + res = e.child("value"); + } else { + throw new Error("Duplicated extension '"+url+"' @ '"+path()+"'"); + } + } + } + return res; + } + + public List extensions(String url) { + List res = new ArrayList(); + loadChildren(); + for (ResourceElement e : children) { + if ("Extension".equals(e.fhirType()) && url.equals(e.primitiveValue("url"))) { + res.add(e); + } + } + return res; + } + + public List extensionValues(String url) { + List res = new ArrayList(); + loadChildren(); + for (ResourceElement e : children) { + if ("Extension".equals(e.fhirType()) && url.equals(e.primitiveValue("url"))) { + if (e.has("value")) { + res.add(e.child("value")); + } + } + } + return res; + } + + +} \ No newline at end of file diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/rendering/ResourceElementTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/rendering/ResourceElementTests.java new file mode 100644 index 000000000..05cb98cbf --- /dev/null +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/rendering/ResourceElementTests.java @@ -0,0 +1,226 @@ +package org.hl7.fhir.r5.test.rendering; + + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.util.List; + +import org.hl7.fhir.exceptions.FHIRFormatError; +import org.hl7.fhir.r5.context.ContextUtilities; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.elementmodel.Manager; +import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; +import org.hl7.fhir.r5.elementmodel.ValidatedFragment; +import org.hl7.fhir.r5.formats.XmlParser; +import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.r5.renderers.utils.ResourceElement; +import org.hl7.fhir.r5.renderers.utils.ResourceElement.ElementKind; +import org.hl7.fhir.r5.test.utils.TestingUtilities; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ResourceElementTests { + + @Test + public void testDirect() throws FHIRFormatError, IOException { + IWorkerContext worker = TestingUtilities.getSharedWorkerContext(); + Resource res = new XmlParser().parse(TestingUtilities.loadTestResource("r5", "bundle-resource-element-test.xml")); + ResourceElement re = new ResourceElement(new ContextUtilities(worker), res); + checkTree(re); + } + + @Test + public void testIndirect() throws FHIRFormatError, IOException { + IWorkerContext worker = TestingUtilities.getSharedWorkerContext(); + List res = Manager.parse(worker, TestingUtilities.loadTestResourceStream("r5", "bundle-resource-element-test.xml"), FhirFormat.XML); + ResourceElement re = new ResourceElement(new ContextUtilities(worker), res.get(0).getElement()); + checkTree(re); + } + + private void checkTree(ResourceElement bnd) { + Assertions.assertTrue(bnd.fhirType().equals("Bundle")); + Assertions.assertNull(bnd.name()); + Assertions.assertEquals("Bundle", bnd.path()); + Assertions.assertEquals(ElementKind.IndependentResource, bnd.kind()); + + ResourceElement type = bnd.child("type"); + Assertions.assertTrue(type.fhirType().equals("code")); + Assertions.assertEquals("type", type.name()); + Assertions.assertEquals("Bundle.type", type.path()); + Assertions.assertTrue(type.isPrimitive()); + Assertions.assertTrue(type.hasPrimitiveValue()); + Assertions.assertEquals("collection", type.primitiveValue()); + Assertions.assertFalse(type.hasChildren()); + Assertions.assertEquals(ElementKind.PrimitiveType, type.kind()); + + ResourceElement id = bnd.child("identifier"); + Assertions.assertEquals("Identifier", id.fhirType()); + Assertions.assertEquals("identifier", id.name()); + Assertions.assertEquals("Bundle.identifier", id.path()); + Assertions.assertFalse(id.isPrimitive()); + Assertions.assertFalse(id.hasPrimitiveValue()); + Assertions.assertTrue(id.hasChildren()); + Assertions.assertEquals(ElementKind.DataType, id.kind()); + + ResourceElement system = id.child("system"); + Assertions.assertEquals("uri", system.fhirType()); + Assertions.assertEquals("system", system.name()); + Assertions.assertEquals("Bundle.identifier.system", system.path()); + Assertions.assertTrue(system.isPrimitive()); + Assertions.assertTrue(system.hasPrimitiveValue()); + Assertions.assertEquals("http://something1", system.primitiveValue()); + Assertions.assertFalse(system.hasChildren()); + Assertions.assertEquals(ElementKind.PrimitiveType, system.kind()); + + ResourceElement value = id.child("value"); + Assertions.assertEquals("string", value.fhirType()); + Assertions.assertEquals("value", value.name()); + Assertions.assertEquals("Bundle.identifier.value", value.path()); + Assertions.assertTrue(value.isPrimitive()); + Assertions.assertTrue(value.hasPrimitiveValue()); + Assertions.assertEquals("something2", value.primitiveValue()); + Assertions.assertFalse(value.hasChildren()); + Assertions.assertEquals(ElementKind.PrimitiveType, value.kind()); + + int i = 0; + for (ResourceElement link : bnd.children("link")) { + checkLink(i, link); + i++; + } + + ResourceElement entry = bnd.child("entry"); + Assertions.assertEquals("Bundle.entry", entry.fhirType()); + Assertions.assertEquals("entry", entry.name()); + Assertions.assertEquals("Bundle.entry[0]", entry.path()); + Assertions.assertFalse(entry.isPrimitive()); + Assertions.assertFalse(entry.hasPrimitiveValue()); + Assertions.assertTrue(entry.hasChildren()); + Assertions.assertEquals(ElementKind.BackboneElement, entry.kind()); + + ResourceElement fu = entry.child("fullUrl"); + Assertions.assertEquals("uri", fu.fhirType()); + Assertions.assertEquals("fullUrl", fu.name()); + Assertions.assertEquals("Bundle.entry[0].fullUrl", fu.path()); + Assertions.assertTrue(fu.isPrimitive()); + Assertions.assertTrue(fu.hasPrimitiveValue()); + Assertions.assertEquals("http://something5", fu.primitiveValue()); + Assertions.assertFalse(fu.hasChildren()); + Assertions.assertEquals(ElementKind.PrimitiveType, fu.kind()); + + ResourceElement obs = entry.child("resource"); + checkObservation(obs); + } + + private void checkObservation(ResourceElement obs) { + Assertions.assertTrue(obs.fhirType().equals("Observation")); + Assertions.assertEquals("resource", obs.name()); + Assertions.assertEquals("Bundle.entry[0].resource", obs.path()); + Assertions.assertEquals(ElementKind.BundleEntry, obs.kind()); + + List children = obs.children(); + assertEquals(3, children.size()); + + checkObsCode(children.get(1)); + + assertEquals(children.get(2), obs.child("value")); + assertEquals(children.get(2), obs.child("value[x]")); + checkObsValue(children.get(2)); + + assertEquals(children.get(0), obs.child("contained")); + checkContained(children.get(0)); + } + + private void checkContained(ResourceElement cont) { + Assertions.assertEquals("Provenance", cont.fhirType()); + Assertions.assertEquals("contained", cont.name()); + Assertions.assertEquals("Bundle.entry[0].resource.contained[0]", cont.path()); + Assertions.assertFalse(cont.isPrimitive()); + Assertions.assertFalse(cont.hasPrimitiveValue()); + Assertions.assertTrue(cont.hasChildren()); + Assertions.assertEquals(ElementKind.ContainedResource, cont.kind()); + } + + private void checkObsValue(ResourceElement obsValue) { + Assertions.assertEquals("Quantity", obsValue.fhirType()); + Assertions.assertEquals("value[x]", obsValue.name()); + Assertions.assertEquals("Bundle.entry[0].resource.value[x]", obsValue.path()); + Assertions.assertFalse(obsValue.isPrimitive()); + Assertions.assertFalse(obsValue.hasPrimitiveValue()); + Assertions.assertTrue(obsValue.hasChildren()); + Assertions.assertEquals(ElementKind.DataType, obsValue.kind()); + } + + private void checkObsCode(ResourceElement obsCode) { + Assertions.assertEquals("CodeableConcept", obsCode.fhirType()); + Assertions.assertEquals("code", obsCode.name()); + Assertions.assertEquals("Bundle.entry[0].resource.code", obsCode.path()); + Assertions.assertFalse(obsCode.isPrimitive()); + Assertions.assertFalse(obsCode.hasPrimitiveValue()); + Assertions.assertTrue(obsCode.hasChildren()); + Assertions.assertEquals(ElementKind.DataType, obsCode.kind()); + + ResourceElement txt = obsCode.children().get(1); + Assertions.assertEquals("string", txt.fhirType()); + Assertions.assertEquals("text", txt.name()); + Assertions.assertEquals("Bundle.entry[0].resource.code.text", txt.path()); + Assertions.assertTrue(txt.isPrimitive()); + Assertions.assertFalse(txt.hasPrimitiveValue()); + Assertions.assertTrue(txt.hasChildren()); + Assertions.assertEquals(ElementKind.PrimitiveType, txt.kind()); + + ResourceElement e1 = txt.extension("http://something11"); + Assertions.assertEquals("Extension", e1.fhirType()); + Assertions.assertEquals("extension", e1.name()); + Assertions.assertEquals("Bundle.entry[0].resource.code.text.extension[0]", e1.path()); + Assertions.assertFalse(e1.isPrimitive()); + Assertions.assertFalse(e1.hasPrimitiveValue()); + Assertions.assertTrue(e1.hasChildren()); + Assertions.assertEquals(ElementKind.DataType, e1.kind()); + Assertions.assertEquals("http://something11", e1.primitiveValue("url")); + + ResourceElement ev = txt.extensionValue("http://something11"); + Assertions.assertEquals(ev, e1.child("value")); + Assertions.assertEquals(ev, e1.child("value[x]")); + + Assertions.assertEquals("string", ev.fhirType()); + Assertions.assertEquals("value[x]", ev.name()); + Assertions.assertEquals("Bundle.entry[0].resource.code.text.extension[0].value[x]", ev.path()); + Assertions.assertTrue(ev.isPrimitive()); + Assertions.assertTrue(ev.hasPrimitiveValue()); + Assertions.assertFalse(ev.hasChildren()); + Assertions.assertEquals(ElementKind.PrimitiveType, ev.kind()); + Assertions.assertEquals("something12", ev.primitiveValue()); + } + + private void checkLink(int i, ResourceElement link) { + Assertions.assertEquals("Bundle.link", link.fhirType()); + Assertions.assertEquals("link", link.name()); + Assertions.assertEquals("Bundle.link["+i+"]", link.path()); + Assertions.assertFalse(link.isPrimitive()); + Assertions.assertFalse(link.hasPrimitiveValue()); + Assertions.assertTrue(link.hasChildren()); + Assertions.assertEquals(ElementKind.BackboneElement, link.kind()); + + ResourceElement rel = link.child("relation"); + Assertions.assertEquals("code", rel.fhirType()); + Assertions.assertEquals("relation", rel.name()); + Assertions.assertEquals("Bundle.link["+i+"].relation", rel.path()); + Assertions.assertTrue(rel.isPrimitive()); + Assertions.assertTrue(rel.hasPrimitiveValue()); + Assertions.assertEquals(i == 0 ? "self" : "next", rel.primitiveValue()); + Assertions.assertFalse(rel.hasChildren()); + Assertions.assertEquals(ElementKind.PrimitiveType, rel.kind()); + + ResourceElement url = link.child("url"); + Assertions.assertEquals("uri", url.fhirType()); + Assertions.assertEquals("url", url.name()); + Assertions.assertEquals("Bundle.link["+i+"].url", url.path()); + Assertions.assertTrue(url.isPrimitive()); + Assertions.assertTrue(url.hasPrimitiveValue()); + Assertions.assertEquals(i == 0 ? "http://something3" : "http://something4", url.primitiveValue()); + Assertions.assertFalse(url.hasChildren()); + Assertions.assertEquals(ElementKind.PrimitiveType, url.kind()); + } + +}