more PE work

This commit is contained in:
Grahame Grieve 2022-12-29 10:18:47 +13:00
parent d532af808b
commit 44ecfbf53d
8 changed files with 361 additions and 25 deletions

View File

@ -228,6 +228,9 @@ public abstract class Base implements Serializable, IBase, IElement {
throw new FHIRException("Attempt to add child with unknown name "+name);
}
public boolean removeChild(String name, Base value) {
throw new FHIRException("Attempt to remove child with unknown name "+name);
}
/**
* Supports iterating the children elements in some generic processor or browser
* All defined children will be listed, even if they have no value on this instance

View File

@ -20,6 +20,7 @@ import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.ResourceFactory;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
import org.hl7.fhir.r5.utils.FHIRPathEngine;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.Utilities;
@ -85,6 +86,7 @@ public class PEBuilder {
private ContextUtilities cu;
private PEElementPropertiesPolicy elementProps;
private boolean fixedPropsDefault;
private FHIRPathEngine fpe;
/**
* @param context - must be loaded with R5 definitions
@ -97,6 +99,28 @@ public class PEBuilder {
this.fixedPropsDefault = fixedPropsDefault;
pu = new ProfileUtilities(context, null, null);
cu = new ContextUtilities(context);
fpe = new FHIRPathEngine(context, pu);
}
/**
* Given a profile, return a tree of the elements defined in the profile model. This builds the profile model
* for the provided version of the nominated profile
*
* The tree of elements in the profile model is different to those defined in the base resource:
* - some elements are removed (max = 0)
* - extensions are turned into named elements
* - slices are turned into named elements
* - element properties - doco, cardinality, binding etc is updated for what the profile says
*
* Warning: profiles and resources are recursive; you can't iterate this tree until it you get
* to the leaves because there are nodes that don't terminate (extensions have extensions)
*
*/
public PEDefinition buildPEDefinition(StructureDefinition profile) {
if (!profile.hasSnapshot()) {
throw new DefinitionException("Profile '"+profile.getVersionedUrl()+"' does not have a snapshot");
}
return new PEDefinitionResource(this, profile);
}
/**
@ -168,6 +192,25 @@ public class PEBuilder {
return loadInstance(defn, resource);
}
/**
* Given a resource and a profile, return a tree of instance data as defined by the profile model
* using the provided version of the profile
*
* The tree is a facade to the underlying resource - all actual data is stored against the resource,
* and retrieved on the fly from the resource, so that applications can work at either level, as
* convenient.
*
* Note that there's a risk that deleting something through the resource while holding
* a handle to a PEInstance that is a facade on what is deleted leaves an orphan facade
* that will continue to function, but is making changes to resource content that is no
* longer part of the resource
*
*/
public PEInstance buildPEInstance(StructureDefinition profile, Resource resource) {
PEDefinition defn = buildPEDefinition(profile);
return loadInstance(defn, resource);
}
/**
* Given a resource and a profile, return a tree of instance data as defined by the profile model
* using the nominated version of the profile
@ -206,6 +249,25 @@ public class PEBuilder {
return res;
}
/**
* For the provided version of a profile, construct a resource and fill out any fixed or required elements
*
* Note that fixed values are filled out irrespective of the value of fixedProps when the builder is created
*
* @param profile the profile
* @param meta whether to mark the profile in Resource.meta.profile
* @return constructed resource
*/
public Resource createResource(StructureDefinition profile, boolean meta) {
PEDefinition definition = buildPEDefinition(profile);
Resource res = ResourceFactory.createResource(definition.types().get(0).getType());
populateByProfile(res, definition);
if (meta) {
res.getMeta().addProfile(definition.profile.getUrl());
}
return res;
}
/**
* For the current version of a profile, construct a resource and fill out any fixed or required elements
*
@ -414,7 +476,7 @@ public class PEBuilder {
}
private PEInstance loadInstance(PEDefinition defn, Resource resource) {
throw new NotImplementedException("Not done yet");
return new PEInstance(this, defn, resource, resource, defn.name());
}
public IWorkerContext getContext() {
@ -496,4 +558,8 @@ public class PEBuilder {
}
return getByName(elements, path);
}
public List<Base> exec(Resource resource, Base data, String fhirpath) {
return fpe.evaluate(this, resource, resource, data, fhirpath);
}
}

View File

@ -228,6 +228,16 @@ public abstract class PEDefinition {
* @return used in the instance processor to differentiate slices
*/
public abstract String fhirpath();
public boolean isList() {
return "*".equals(definition.getBase().getMax());
}
public boolean repeats() {
return max() > 1;
}
}

View File

@ -57,7 +57,7 @@ public class PEDefinitionExtension extends PEDefinition {
if (ved.isRequired() || eed.isProhibited()) {
return "extension('"+extension.getUrl()+"').value";
} else {
return "extension('"+extension.getUrl()+"').extension";
return "extension('"+extension.getUrl()+"')";
}
}

View File

@ -1,21 +1,46 @@
package org.hl7.fhir.r5.profilemodel;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r5.model.Base;
import org.hl7.fhir.r5.model.BaseDateTimeType;
import org.hl7.fhir.r5.model.CodeableConcept;
import org.hl7.fhir.r5.model.ContactPoint;
import org.hl7.fhir.r5.model.Identifier;
import org.hl7.fhir.r5.model.DataType;
import org.hl7.fhir.r5.model.DateTimeType;
import org.hl7.fhir.r5.model.HumanName;
import org.hl7.fhir.r5.model.Address;
import org.hl7.fhir.r5.model.PrimitiveType;
import org.hl7.fhir.r5.model.Quantity;
import org.hl7.fhir.r5.model.Reference;
import org.hl7.fhir.r5.model.Resource;
public abstract class PEInstance {
/**
* This class provides a profile centric view of a resource, as driven by a profile
*
* This class is also suitable to be used as the base of a POJO
* @author grahamegrieve
*
*/
public class PEInstance {
private PEBuilder builder;
private PEDefinition definition;
private Resource resource; // for FHIRPath
private Base data;
private String path;
protected PEInstance(PEDefinition definition, Base data) {
protected PEInstance(PEBuilder builder, PEDefinition definition, Resource resource, Base data, String path) {
super();
this.builder = builder;
this.definition = definition;
this.resource = resource;
this.data = data;
this.path = path;
}
/**
@ -37,60 +62,178 @@ public abstract class PEInstance {
*/
public List<PEInstance> children() {
List<PEInstance> res = new ArrayList<>();
for (PEDefinition child : definition.children()) {
List<Base> instances = builder.exec(resource, data, child.fhirpath());
int i = 0;
for (Base b : instances) {
res.add(new PEInstance(builder, child, resource, b, path+"."+child.name()+(child.repeats() ? "["+i+"]": "")));
i++;
}
}
return res;
}
/**
* @return all the single children of this instance data for the named property. An exception if there's more than one, null if there's none
*/
public PEInstance child(String name) {
PEDefinition child = byName(definition.children(), name);
List<Base> instances = builder.exec(resource, data, child.fhirpath());
if (instances.isEmpty()) {
return null;
} else if (instances.size() == 1) {
return new PEInstance(builder, child, resource, instances.get(0), path+"."+child.name()+(child.repeats() ? "[0]": ""));
} else {
throw new FHIRException("Found multiple instances for "+name+"@ "+path);
}
}
/**
* @return all the children of this instance data for the named property
*/
public List<PEInstance> children(String name) {
// PEDefinition child = definition.childByName(name);
// if (child = null) {
//
// }
return null;
PEDefinition child = byName(definition.children(), name);
List<PEInstance> res = new ArrayList<>();
List<Base> instances = builder.exec(resource, data, child.fhirpath());
int i = 0;
for (Base b : instances) {
res.add(new PEInstance(builder, child, resource, b, path+"."+child.name()+(child.repeats() ? "["+i+"]": "")));
i++;
}
return res;
}
private PEDefinition byName(List<PEDefinition> children, String name) {
for (PEDefinition defn : children) {
if (defn.name().equals(name)) {
return defn;
}
}
throw new FHIRException("No children with the name '"+name+"'");
}
/**
* @return all the children of this instance data with the named property and the named type (for polymorphic
*/
public abstract List<PEInstance> children(String name, String type);
/**
* @return make a child, and append it to existing children (if they exist)
*/
public abstract PEInstance makeChild(String name);
public PEInstance makeChild(String name) {
PEDefinition child = byName(definition.children(), name);
Base b = data.addChild(child.schemaName());
builder.populateByProfile(b, child);
return new PEInstance(builder, child, resource, b, path+"."+child.name());
}
/**
* @return get a child. if it doesn't exist, make one
*/
public PEInstance forceChild(String name) {
PEDefinition child = byName(definition.children(), name);
List<Base> instances = builder.exec(resource, data, child.fhirpath());
if (instances.isEmpty()) {
Base b = data.addChild(child.schemaName());
builder.populateByProfile(b, child);
return new PEInstance(builder, child, resource, b, path+"."+child.name()+(child.isList() ? "[0]": ""));
} else {
return new PEInstance(builder, child, resource, instances.get(0), path+"."+child.name()+(child.repeats() ? "[0]": ""));
}
}
/**
* remove the nominated child from the resource
*/
public abstract void removeChild(PEInstance child);
public boolean removeChild(PEInstance child) {
return data.removeChild(child.definition().schemaName(), child.data);
}
public enum PEInstanceDataKind {
Resource, Complex, DataType, PrimitiveValue
Resource, Complex, DataType, Primitive
}
/**
* @return the kind of data behind this profiled node
*/
public abstract PEInstanceDataKind getDataKind();
public PEInstanceDataKind getDataKind() {
if (data instanceof Resource) {
return PEInstanceDataKind.Resource;
}
if (data instanceof PrimitiveType) {
return PEInstanceDataKind.Primitive;
}
if (data instanceof DataType) {
return PEInstanceDataKind.DataType;
}
return PEInstanceDataKind.Complex;
}
public Base data() {
return data;
}
/**
* @return if dataKind = Resource, get the underlying resource, otherwise an exception
*/
public abstract Resource asResource();
public Resource asResource() {
return (Resource) data;
}
/**
* @return if dataKind = Datatype, get the underlying resource, otherwise an exception
*/
public abstract DataType asDataType();
public DataType asDataType() {
return (DataType) data;
}
public CodeableConcept asCodeableConcept() {
return (CodeableConcept) asDataType();
}
public Identifier Identifier() {
return (Identifier) asDataType();
}
public Quantity asQuantity() {
return (Quantity) asDataType();
}
public HumanName asHumanName() {
return (HumanName) asDataType();
}
public Address Address() {
return (Address) asDataType();
}
public ContactPoint asContactPoint() {
return (ContactPoint) asDataType();
}
public Reference asReference() {
return (Reference) asDataType();
}
/**
* @return if dataKind = PrimitiveValue, get the underlying resource, otherwise an exception
*
* Note that this is for e.g. String.value, not String itself
*/
public abstract String getPrimitiveValue();
public String getPrimitiveAsString() {
return data.primitiveValue();
}
public Date getPrimitiveAsDate() {
if (data instanceof BaseDateTimeType) {
return ((DateTimeType) data).getValue();
}
return null;
}
public void setPrimitiveValue(String value) {
PrimitiveType<?> pt = (PrimitiveType<?>) data;
pt.setValueAsString(value);
}
public String getPath() {
return path;
}
}

View File

@ -0,0 +1,17 @@
package org.hl7.fhir.r5.profilemodel.gen;
import org.hl7.fhir.r5.profilemodel.PEInstance;
public class PEGeneratedBase {
protected PEInstance instance;
protected void removeChild(String string) {
PEInstance child = instance.child("simple");
if (child != null) {
instance.removeChild(child);
}
}
}

View File

@ -0,0 +1,49 @@
package org.hl7.fhir.r5.profilemodel.gen;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.model.CodeType;
import org.hl7.fhir.r5.model.Observation;
import org.hl7.fhir.r5.profilemodel.PEBuilder;
import org.hl7.fhir.r5.profilemodel.PEBuilder.PEElementPropertiesPolicy;
import org.hl7.fhir.r5.profilemodel.PEInstance;
/**
* This class is a manually written example of the code that a POJO code
* generator for Profiles would produce
*
* @author grahamegrieve
*
*/
public class ProfileExample extends PEGeneratedBase {
public ProfileExample(IWorkerContext context, Observation observation) {
super();
PEBuilder builder = new PEBuilder(context, PEElementPropertiesPolicy.EXTENSION_ID, true);
instance = builder.buildPEInstance("http://hl7.org/fhir/test/StructureDefinition/pe-profile1", "0.1", observation);
}
/**
* Extension http://hl7.org/fhir/test/StructureDefinition/pe-extension-simple, type code
* @return
*/
public CodeType getSimple() {
return (CodeType) instance.forceChild("simple").asDataType();
}
public boolean hasSimple() {
return instance.child("simple") != null;
}
public ProfileExample clearSimple() {
removeChild("simple");
return this;
}
/*
* this doesn't exist, because of the way infrastructure works.
* You get the value and set the properties
*/
// public void setSimple() {
// return (CodeType) instance.forceChild("simple").asDataType();
// }
}

View File

@ -6,12 +6,15 @@ import java.util.List;
import org.hl7.fhir.r5.context.IWorkerContext;
import org.hl7.fhir.r5.formats.IParser.OutputStyle;
import org.hl7.fhir.r5.formats.JsonParser;
import org.hl7.fhir.r5.model.Coding;
import org.hl7.fhir.r5.model.Observation;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.profilemodel.PEDefinition;
import org.hl7.fhir.r5.profilemodel.PEInstance;
import org.hl7.fhir.r5.profilemodel.PEType;
import org.hl7.fhir.r5.profilemodel.PEBuilder;
import org.hl7.fhir.r5.profilemodel.PEBuilder.PEElementPropertiesPolicy;
import org.hl7.fhir.r5.profilemodel.PEInstance.PEInstanceDataKind;
import org.hl7.fhir.r5.test.utils.TestPackageLoader;
import org.hl7.fhir.r5.test.utils.TestingUtilities;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
@ -66,7 +69,7 @@ public class PETests {
checkElement(children.get(4), "contained", "contained", 0, Integer.MAX_VALUE, false, "http://hl7.org/fhir/StructureDefinition/Resource", 4, "contained");
checkElement(children.get(5), "extension", "extension", 0, Integer.MAX_VALUE, false, "http://hl7.org/fhir/StructureDefinition/Extension", 3, "extension.where(((url = 'http://hl7.org/fhir/test/StructureDefinition/pe-extension-simple') or (url = 'http://hl7.org/fhir/test/StructureDefinition/pe-extension-complex')).not())");
checkElement(children.get(6), "extension", "simple", 0, 1, false, "http://hl7.org/fhir/StructureDefinition/code", 2, "extension('http://hl7.org/fhir/test/StructureDefinition/pe-extension-simple').value");
checkElement(children.get(7), "extension", "complex", 0, 1, false, "http://hl7.org/fhir/StructureDefinition/Extension", 4, "extension('http://hl7.org/fhir/test/StructureDefinition/pe-extension-complex').extension");
checkElement(children.get(7), "extension", "complex", 0, 1, false, "http://hl7.org/fhir/StructureDefinition/Extension", 4, "extension('http://hl7.org/fhir/test/StructureDefinition/pe-extension-complex')");
checkElement(children.get(8), "identifier", "identifier", 0, 1, false, "http://hl7.org/fhir/StructureDefinition/Identifier", 7, "identifier");
checkElement(children.get(9), "status", "status", 1, 1, true, "http://hl7.org/fhir/StructureDefinition/code", 2, "status");
checkElement(children.get(10), "category", "category", 0, Integer.MAX_VALUE, false, "http://hl7.org/fhir/StructureDefinition/CodeableConcept", 3, "category");
@ -285,8 +288,7 @@ public class PETests {
}
}
}
}
}
@Test
public void testCreate() throws IOException {
@ -301,4 +303,50 @@ public class PETests {
System.out.println(json);
}
@Test
public void testLoad() throws IOException {
load();
Resource res = new JsonParser().parse(TestingUtilities.loadTestResource("R5", "pe-observation-1.json"));
PEInstance obs = new PEBuilder(ctxt, PEElementPropertiesPolicy.EXTENSION, true).buildPEInstance("http://hl7.org/fhir/test/StructureDefinition/pe-profile1", res);
PEInstance status = obs.child("status");
Assertions.assertNotNull(status);
Assertions.assertEquals("TestProfile.status", status.getPath());
Assertions.assertEquals(PEInstanceDataKind.Primitive, status.getDataKind());
Assertions.assertEquals("final", status.asDataType().primitiveValue());
Assertions.assertEquals("final", status.getPrimitiveAsString());
PEInstance code = obs.child("code");
Assertions.assertNotNull(code);
Assertions.assertEquals("TestProfile.code", code.getPath());
Assertions.assertEquals(PEInstanceDataKind.DataType, code.getDataKind());
Assertions.assertEquals("76690-7", code.asCodeableConcept().getCodingFirstRep().getCode());
PEInstance simple = obs.child("simple");
Assertions.assertNotNull(simple);
Assertions.assertEquals("TestProfile.simple", simple.getPath());
Assertions.assertEquals(PEInstanceDataKind.Primitive, simple.getDataKind());
Assertions.assertEquals("14647-2", simple.getPrimitiveAsString());
PEInstance complex = obs.child("complex");
Assertions.assertNotNull(complex);
Assertions.assertEquals("TestProfile.complex", complex.getPath());
Assertions.assertEquals(PEInstanceDataKind.DataType, complex.getDataKind());
PEInstance slice1 = complex.child("slice1");
Assertions.assertNotNull(slice1);
Assertions.assertEquals("TestProfile.complex.slice1[0]", slice1.getPath());
Assertions.assertEquals(PEInstanceDataKind.DataType, slice1.getDataKind());
Assertions.assertEquals("18767-4", ((Coding) slice1.asDataType()).getCode());
PEInstance slice2 = complex.child("slice2");
Assertions.assertNotNull(slice2);
Assertions.assertEquals("TestProfile.complex.slice2[0]", slice2.getPath());
Assertions.assertEquals(PEInstanceDataKind.Primitive, slice2.getDataKind());
Assertions.assertEquals("A string value", slice2.getPrimitiveAsString());
}
}