5401 add terserutil clear by fhirpath (#5402)

* begin with failing test

* test passes

* test passes

* changelog

* moar testor
This commit is contained in:
Ken Stevens 2023-10-25 23:35:20 -04:00 committed by GitHub
parent 84e3becf1c
commit a7a446903f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 210 additions and 122 deletions

View File

@ -20,6 +20,7 @@
package ca.uhn.fhir.context;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.model.api.annotation.Child;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseReference;
@ -78,6 +79,10 @@ public abstract class BaseRuntimeChildDefinition {
this.myReplacedParentDefinition = myReplacedParentDefinition;
}
public boolean isMultipleCardinality() {
return this.getMax() > 1 || this.getMax() == Child.MAX_UNLIMITED;
}
public interface IAccessor {
List<IBase> getValues(IBase theTarget);

View File

@ -39,7 +39,6 @@ import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.model.api.Tag;
import ca.uhn.fhir.model.api.TagList;
import ca.uhn.fhir.model.api.annotation.Child;
import ca.uhn.fhir.model.base.composite.BaseCodingDt;
import ca.uhn.fhir.model.base.composite.BaseContainedDt;
import ca.uhn.fhir.model.primitive.IdDt;
@ -659,9 +658,8 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
theEventWriter.endArray();
}
BaseRuntimeChildDefinition replacedParentDefinition = nextChild.getReplacedParentDefinition();
if (isMultipleCardinality(nextChild.getMax())
|| (replacedParentDefinition != null
&& isMultipleCardinality(replacedParentDefinition.getMax()))) {
if (nextChild.isMultipleCardinality()
|| (replacedParentDefinition != null && replacedParentDefinition.isMultipleCardinality())) {
beginArray(theEventWriter, nextChildSpecificName);
inArray = true;
encodeChildElementToStreamWriter(
@ -728,14 +726,14 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
List<HeldExtension> heldModExts = Collections.emptyList();
if (extensions.size() > i
&& extensions.get(i) != null
&& extensions.get(i).isEmpty() == false) {
&& !extensions.get(i).isEmpty()) {
haveContent = true;
heldExts = extensions.get(i);
}
if (modifierExtensions.size() > i
&& modifierExtensions.get(i) != null
&& modifierExtensions.get(i).isEmpty() == false) {
&& !modifierExtensions.get(i).isEmpty()) {
haveContent = true;
heldModExts = modifierExtensions.get(i);
}
@ -746,7 +744,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
} else {
nextComments = null;
}
if (nextComments != null && nextComments.isEmpty() == false) {
if (nextComments != null && !nextComments.isEmpty()) {
haveContent = true;
}
@ -804,10 +802,6 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
return myIsSupportsFhirComment;
}
private boolean isMultipleCardinality(int maxCardinality) {
return maxCardinality > 1 || maxCardinality == Child.MAX_UNLIMITED;
}
private void encodeCompositeElementToStreamWriter(
RuntimeResourceDefinition theResDef,
IBaseResource theResource,
@ -917,10 +911,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
// Undeclared extensions
extractUndeclaredExtensions(
theResourceId, extensions, modifierExtensions, null, null, theEncodeContext, theContainedResource);
boolean haveExtension = false;
if (!extensions.isEmpty()) {
haveExtension = true;
}
boolean haveExtension = !extensions.isEmpty();
if (theResourceId.hasFormatComment() || haveExtension) {
beginObject(theEventWriter, "_id");

View File

@ -351,6 +351,29 @@ public final class TerserUtil {
clear(childDefinition.getAccessor().getValues(theResource));
}
/**
* Clears the specified field on the resource provided by the FHIRPath. If more than one value matches
* the FHIRPath, all values will be cleared.
*
* @param theFhirContext
* @param theResource
* @param theFhirPath
*/
public static void clearFieldByFhirPath(FhirContext theFhirContext, IBaseResource theResource, String theFhirPath) {
if (theFhirPath.contains(".")) {
String parentPath = theFhirPath.substring(0, theFhirPath.lastIndexOf("."));
String fieldName = theFhirPath.substring(theFhirPath.lastIndexOf(".") + 1);
FhirTerser terser = theFhirContext.newTerser();
List<IBase> parents = terser.getValues(theResource, parentPath);
for (IBase parent : parents) {
clearField(theFhirContext, fieldName, parent);
}
} else {
clearField(theFhirContext, theResource, theFhirPath);
}
}
/**
* Clears the specified field on the element provided
*
@ -362,7 +385,16 @@ public final class TerserUtil {
BaseRuntimeElementDefinition definition = theFhirContext.getElementDefinition(theBase.getClass());
BaseRuntimeChildDefinition childDefinition = definition.getChildByName(theFieldName);
Validate.notNull(childDefinition);
clear(childDefinition.getAccessor().getValues(theBase));
BaseRuntimeChildDefinition.IAccessor accessor = childDefinition.getAccessor();
clear(accessor.getValues(theBase));
List<IBase> newValue = accessor.getValues(theBase);
if (newValue != null && !newValue.isEmpty()) {
// Our clear failed, probably because it was an immutable SingletonList returned by a FieldPlainAccessor
// that cannot be cleared.
// Let's just null it out instead.
childDefinition.getMutator().setValue(theBase, null);
}
}
/**

View File

@ -0,0 +1,5 @@
---
type: add
issue: 5401
title: "Previously, it was only possible to clear a top-level field on a resource using TerserUtil. A new method has been added
to TerserUtil to support clearing a value by FhirPath."

View File

@ -13,6 +13,7 @@ import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.PrimitiveType;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.Test;
import java.util.Date;
@ -30,88 +31,88 @@ class TerserUtilTest {
private FhirContext ourFhirContext = FhirContext.forR4();
private static final String SAMPLE_PERSON =
"""
{
"resourceType": "Patient",
"extension": [
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
"valueCoding": {
"system": "MyInternalRace",
"code": "X",
"display": "Eks"
"""
{
"resourceType": "Patient",
"extension": [
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
"valueCoding": {
"system": "MyInternalRace",
"code": "X",
"display": "Eks"
}
},
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity'",
"valueCoding": {
"system": "MyInternalEthnicity",
"display": "NNN"
}
}
},
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity'",
"valueCoding": {
"system": "MyInternalEthnicity",
"display": "NNN"
],
"identifier": [
{
"system": "http://example.org/member_id",
"value": "123123"
},
{
"system": "http://example.org/medicaid_id",
"value": "12312323123Z"
},
{
"system": "http://example.org/CDNS_id",
"value": "123123123E"
},
{
"system": "http://example.org/SSN"
}
}
],
"identifier": [
{
"system": "http://example.org/member_id",
"value": "123123"
},
{
"system": "http://example.org/medicaid_id",
"value": "12312323123Z"
},
{
"system": "http://example.org/CDNS_id",
"value": "123123123E"
},
{
"system": "http://example.org/SSN"
}
],
"active": true,
"name": [
{
"family": "TestFamily",
"given": [
"Given"
]
}
],
"telecom": [
{
"system": "email",
"value": "email@email.io"
},
{
"system": "phone",
"value": "123123231"
},
{
"system": "phone",
"value": "1231232312"
},
{
"system": "phone",
"value": "1231232314"
}
],
"gender": "male",
"birthDate": "1900-01-01",
"deceasedBoolean": true,
"contained": [
{
"id": "1",
"identifier": [
{
"system": "urn:hssc:srhs:contact:organizationId",
"value": "1000"
}
],
"name": "BUILDERS FIRST SOURCE",
"resourceType": "Organization"
}
]
}
""";
],
"active": true,
"name": [
{
"family": "TestFamily",
"given": [
"Given"
]
}
],
"telecom": [
{
"system": "email",
"value": "email@email.io"
},
{
"system": "phone",
"value": "123123231"
},
{
"system": "phone",
"value": "1231232312"
},
{
"system": "phone",
"value": "1231232314"
}
],
"gender": "male",
"birthDate": "1900-01-01",
"deceasedBoolean": true,
"contained": [
{
"id": "1",
"identifier": [
{
"system": "urn:hssc:srhs:contact:organizationId",
"value": "1000"
}
],
"name": "BUILDERS FIRST SOURCE",
"resourceType": "Organization"
}
]
}
""";
@Test
void testCloneEidIntoResource() {
@ -180,7 +181,7 @@ class TerserUtilTest {
RuntimeResourceDefinition definition = p1Helper.getResourceDefinition();
TerserUtil.cloneEidIntoResource(ourFhirContext, definition.getChildByName("identifier"),
p1.getIdentifier().get(0), p2Helper.getResource());
p1.getIdentifier().get(0), p2Helper.getResource());
assertEquals(1, p2Helper.getFieldValues("identifier").size());
@ -291,12 +292,12 @@ class TerserUtilTest {
Patient p1 = new Patient();
p1.addAddress()
.addLine("10 Main Street")
.setCity("Hamilton")
.setState("ON")
.setPostalCode("Z0Z0Z0")
.setCountry("Canada")
.addExtension(ext);
.addLine("10 Main Street")
.setCity("Hamilton")
.setState("ON")
.setPostalCode("Z0Z0Z0")
.setCountry("Canada")
.addExtension(ext);
Patient p2 = new Patient();
p2.addAddress().addLine("10 Lenin Street").setCity("Severodvinsk").setCountry("Russia");
@ -328,12 +329,12 @@ class TerserUtilTest {
Patient p1 = new Patient();
p1.addAddress()
.addLine("10 Main Street")
.setCity("Hamilton")
.setState("ON")
.setPostalCode("Z0Z0Z0")
.setCountry("Canada")
.addExtension(ext);
.addLine("10 Main Street")
.setCity("Hamilton")
.setState("ON")
.setPostalCode("Z0Z0Z0")
.setCountry("Canada")
.addExtension(ext);
Patient p2 = new Patient();
p2.addAddress().addLine("10 Lenin Street").setCity("Severodvinsk").setCountry("Russia");
@ -353,21 +354,21 @@ class TerserUtilTest {
Patient p1 = new Patient();
p1.addAddress()
.addLine("10 Main Street")
.setCity("Hamilton")
.setState("ON")
.setPostalCode("Z0Z0Z0")
.setCountry("Canada")
.addExtension(ext);
.addLine("10 Main Street")
.setCity("Hamilton")
.setState("ON")
.setPostalCode("Z0Z0Z0")
.setCountry("Canada")
.addExtension(ext);
Patient p2 = new Patient();
p2.addAddress()
.addLine("10 Main Street")
.setCity("Hamilton")
.setState("ON")
.setPostalCode("Z0Z0Z1")
.setCountry("Canada")
.addExtension(ext);
.addLine("10 Main Street")
.setCity("Hamilton")
.setState("ON")
.setPostalCode("Z0Z0Z1")
.setCountry("Canada")
.addExtension(ext);
TerserUtil.mergeField(ourFhirContext, "address", p1, p2);
@ -469,7 +470,8 @@ class TerserUtilTest {
assertEquals(1, p2.getName().size());
assertEquals("Doe", p2.getName().get(0).getFamily());
}
@Test
@Test
public void testReplaceFieldByEmptyValue() {
Patient p1 = new Patient();
Patient p2 = new Patient();
@ -507,6 +509,7 @@ class TerserUtilTest {
{
Patient p1 = new Patient();
p1.addName().setFamily("Doe");
assertEquals(1, p1.getName().size());
TerserUtil.clearField(ourFhirContext, p1, "name");
@ -517,6 +520,7 @@ class TerserUtilTest {
Address a1 = new Address();
a1.addLine("Line 1");
a1.addLine("Line 2");
assertEquals(2, a1.getLine().size());
a1.setCity("Test");
TerserUtil.clearField(ourFhirContext, "line", a1);
@ -525,6 +529,47 @@ class TerserUtilTest {
}
}
@Test
public void testClearFieldByFhirPath() {
Patient p1 = new Patient();
p1.addName().setFamily("Doe");
assertEquals(1, p1.getName().size());
TerserUtil.clearFieldByFhirPath(ourFhirContext, p1, "name");
assertEquals(0, p1.getName().size());
Address a1 = new Address();
a1.addLine("Line 1");
a1.addLine("Line 2");
assertEquals(2, a1.getLine().size());
a1.setCity("Test");
a1.getPeriod().setStartElement(new DateTimeType("2021-01-01"));
p1.addAddress(a1);
assertEquals("2021-01-01", p1.getAddress().get(0).getPeriod().getStartElement().toHumanDisplay());
assertNotNull(p1.getAddress().get(0).getPeriod().getStart());
Address a2 = new Address();
a2.addLine("Line 1");
a2.addLine("Line 2");
a2.setCity("Test");
a2.getPeriod().setStartElement(new DateTimeType("2021-01-01"));
p1.addAddress(a2);
TerserUtil.clearFieldByFhirPath(ourFhirContext, p1, "address.line");
TerserUtil.clearFieldByFhirPath(ourFhirContext, p1, "address.period.start");
assertNull(p1.getAddress().get(0).getPeriod().getStart());
assertEquals(2, p1.getAddress().size());
assertEquals(0, p1.getAddress().get(0).getLine().size());
assertEquals(0, p1.getAddress().get(1).getLine().size());
assertEquals("Test", p1.getAddress().get(0).getCity());
assertEquals("Test", p1.getAddress().get(1).getCity());
}
@Test
public void testSetField() {
Patient p1 = new Patient();
@ -551,6 +596,16 @@ class TerserUtilTest {
assertEquals("CITY", p1.getAddress().get(0).getCity());
}
@Test
public void testSetFieldByCompositeFhirPath() {
Patient p1 = new Patient();
TerserUtil.setFieldByFhirPath(ourFhirContext, "address.city", p1, new StringType("CITY"));
assertEquals(1, p1.getAddress().size());
assertEquals("CITY", p1.getAddress().get(0).getCity());
}
@Test
public void testClone() {
Patient p1 = new Patient();