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());
+ }
+
+
}