First draft of ResourceElement - rewrite rendering layer

This commit is contained in:
Grahame Grieve 2024-06-09 18:17:51 +10:00
parent 03ea625a7c
commit 093758429a
2 changed files with 589 additions and 0 deletions

View File

@ -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<ResourceElement> 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<ResourceElement> children() {
loadChildren();
return children;
}
public List<ResourceElement> children(String name) {
loadChildren();
List<ResourceElement> list = new ArrayList<ResourceElement>();
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<ResourceElement> extensions(String url) {
List<ResourceElement> res = new ArrayList<ResourceElement>();
loadChildren();
for (ResourceElement e : children) {
if ("Extension".equals(e.fhirType()) && url.equals(e.primitiveValue("url"))) {
res.add(e);
}
}
return res;
}
public List<ResourceElement> extensionValues(String url) {
List<ResourceElement> res = new ArrayList<ResourceElement>();
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;
}
}

View File

@ -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<ValidatedFragment> 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<ResourceElement> 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());
}
}