diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Base.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Base.java index 34a52388f..a244b2b2f 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Base.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Base.java @@ -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 diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEBuilder.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEBuilder.java index cf3709408..11b814ae8 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEBuilder.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEBuilder.java @@ -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 exec(Resource resource, Base data, String fhirpath) { + return fpe.evaluate(this, resource, resource, data, fhirpath); + } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEDefinition.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEDefinition.java index f3cf37a5b..786be6b9b 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEDefinition.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEDefinition.java @@ -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; + } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEDefinitionExtension.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEDefinitionExtension.java index f08828131..d06c80d63 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEDefinitionExtension.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEDefinitionExtension.java @@ -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()+"')"; } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEInstance.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEInstance.java index 121cef08f..31fe35180 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEInstance.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/PEInstance.java @@ -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 children() { List res = new ArrayList<>(); - + for (PEDefinition child : definition.children()) { + List 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 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 children(String name) { -// PEDefinition child = definition.childByName(name); -// if (child = null) { -// -// } - return null; + PEDefinition child = byName(definition.children(), name); + List res = new ArrayList<>(); + List 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 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 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 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; + } } diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/gen/PEGeneratedBase.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/gen/PEGeneratedBase.java new file mode 100644 index 000000000..dfd66b2a8 --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/gen/PEGeneratedBase.java @@ -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); + } + } + +} + diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/gen/ProfileExample.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/gen/ProfileExample.java new file mode 100644 index 000000000..f7b8805fb --- /dev/null +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/profilemodel/gen/ProfileExample.java @@ -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(); +// } +} diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/profiles/PETests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/profiles/PETests.java index 2f2ff1aa4..e55bc8dc0 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/profiles/PETests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/profiles/PETests.java @@ -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()); + } + + }