diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java index d10a4497b9d..d39ae82b8db 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java @@ -28,6 +28,7 @@ import org.hl7.fhir.instance.model.api.IBaseExtension; import org.hl7.fhir.instance.model.api.IBaseHasExtensions; import java.util.List; +import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -103,14 +104,44 @@ public class ExtensionUtil { * @param theExtensionUrl URL of the extension to get. Must be non-null * @return Returns the first available extension with the specified URL, or null if such extension doesn't exist */ - public static IBaseExtension getExtension(IBaseHasExtensions theBase, String theExtensionUrl) { - return theBase.getExtension() + public static IBaseExtension getExtension(IBase theBase, String theExtensionUrl) { + return validateExtensionSupport(theBase) + .getExtension() .stream() .filter(e -> theExtensionUrl.equals(e.getUrl())) .findFirst() .orElse(null); } + /** + * Gets all extensions that match the specified filter predicate + * + * @param theBase The resource to get the extension for + * @param theFilter Predicate to match the extension against + * @return Returns all extension with the specified URL, or an empty list if such extensions do not exist + */ + public static List> getExtensions(IBase theBase, Predicate theFilter) { + return validateExtensionSupport(theBase) + .getExtension() + .stream() + .filter(theFilter) + .collect(Collectors.toList()); + } + + /** + * Gets all extensions with the specified URL + * + * @param theBase The resource to get the extension for + * @return Returns all extension with the specified URL, or an empty list if such extensions do not exist + */ + public static List> clearExtensions(IBase theBase, Predicate theFilter) { + List> retVal = getExtensions(theBase, theFilter); + validateExtensionSupport(theBase) + .getExtension() + .removeIf(theFilter); + return retVal; + } + /** * Gets all extensions with the specified URL * @@ -119,10 +150,8 @@ public class ExtensionUtil { * @return Returns all extension with the specified URL, or an empty list if such extensions do not exist */ public static List> getExtensions(IBaseHasExtensions theBase, String theExtensionUrl) { - return theBase.getExtension() - .stream() - .filter(e -> theExtensionUrl.equals(e.getUrl())) - .collect(Collectors.toList()); + Predicate urlEqualityPredicate = e -> theExtensionUrl.equals(e.getUrl()); + return getExtensions(theBase, urlEqualityPredicate); } /** diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtil.java index 6d204df45e3..148764974ba 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtil.java @@ -97,12 +97,12 @@ public final class TerserUtil { } /** - * get the Values of a specified field. + * Gets all values of the specified field. * * @param theFhirContext Context holding resource definition * @param theResource Resource to check if the specified field is set * @param theFieldName name of the field to check - * @return Returns true if field exists and has any values set, and false otherwise + * @return Returns all values for the specified field or null if field with the provided name doesn't exist */ public static List getValues(FhirContext theFhirContext, IBaseResource theResource, String theFieldName) { RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResource); @@ -114,6 +114,23 @@ public final class TerserUtil { return resourceIdentifier.getAccessor().getValues(theResource); } + /** + * Gets the first available value for the specified field. + * + * @param theFhirContext Context holding resource definition + * @param theResource Resource to check if the specified field is set + * @param theFieldName name of the field to check + * @return Returns the first value for the specified field or null if field with the provided name doesn't exist or + * has no values + */ + public static IBase getValue(FhirContext theFhirContext, IBaseResource theResource, String theFieldName) { + List values = getValues(theFhirContext, theResource, theFieldName); + if (values == null || values.isEmpty()) { + return null; + } + return values.get(0); + } + /** * Clones specified composite field (collection). Composite field values must conform to the collections * contract. @@ -256,6 +273,20 @@ public final class TerserUtil { childDefinition.getAccessor().getValues(theResource).clear(); } + /** + * Sets the provided field with the given values. This method will add to the collection of existing field values + * in case of multiple cardinality. Use {@link #clearField(FhirContext, FhirTerser, String, IBaseResource, IBase...)} + * to remove values before setting + * + * @param theFhirContext Context holding resource definition + * @param theFieldName Child field name of the resource to set + * @param theResource The resource to set the values on + * @param theValues The values to set on the resource child field name + */ + public static void setField(FhirContext theFhirContext, String theFieldName, IBaseResource theResource, IBase... theValues) { + setField(theFhirContext, theFhirContext.newTerser(), theFieldName, theResource, theValues); + } + /** * Sets the provided field with the given values. This method will add to the collection of existing field values * in case of multiple cardinality. Use {@link #clearField(FhirContext, FhirTerser, String, IBaseResource, IBase...)} @@ -303,6 +334,18 @@ public final class TerserUtil { setFieldByFhirPath(theFhirContext.newTerser(), theFhirPath, theResource, theValue); } + public static List getFieldByFhirPath(FhirContext theFhirContext, String theFhirPath, IBaseResource theResource) { + return theFhirContext.newTerser().getValues(theResource, theFhirPath, false, false); + } + + public static IBase getFirstFieldByFhirPath(FhirContext theFhirContext, String theFhirPath, IBaseResource theResource) { + List values = getFieldByFhirPath(theFhirContext, theFhirPath, theResource); + if (values == null || values.isEmpty()) { + return null; + } + return values.get(0); + } + private static void replaceField(IBaseResource theFrom, IBaseResource theTo, BaseRuntimeChildDefinition childDefinition) { childDefinition.getAccessor().getFirstValueOrNull(theFrom).ifPresent(v -> { childDefinition.getMutator().setValue(theTo, v); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtilHelper.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtilHelper.java index 3cecd163bcc..8fbcdf6ea8a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtilHelper.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtilHelper.java @@ -111,12 +111,27 @@ public class TerserUtilHelper { * Gets values of the specified field. * * @param theField The field to get values from - * @return Returns a collection of values containing values or null if the spefied field doesn't exist + * @return Returns a collection of values containing values or null if the specified field doesn't exist */ public List getFieldValues(String theField) { return TerserUtil.getValues(myContext, myResource, theField); } + /** + * Gets first available values of the specified field. + * + * @param theField The field to get values from + * @return Returns the first available value for the field name or null if the + * specified field doesn't exist or has no values + */ + public IBase getFieldValue(String theField) { + List values = getFieldValues(theField); + if (values == null || values.isEmpty()) { + return null; + } + return values.get(0); + } + /** * Gets the terser instance, creating one if necessary. * diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/ResourceFlatteningInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/ResourceFlatteningInterceptor.java new file mode 100644 index 00000000000..064bb60981f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/ResourceFlatteningInterceptor.java @@ -0,0 +1,148 @@ +package ca.uhn.fhir.jpa.interceptor; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Interceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.model.primitive.CodeDt; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.util.ExtensionUtil; +import ca.uhn.fhir.util.TerserUtil; +import ca.uhn.fhir.util.TerserUtilHelper; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseExtension; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +@Interceptor +public class ResourceFlatteningInterceptor { + + private static final Logger ourLog = LoggerFactory.getLogger(ResourceFlatteningInterceptor.class); + + @Autowired + private FhirContext myFhirContext; + + @Autowired + private DaoRegistry myDaoRegistry; + + public ResourceFlatteningInterceptor(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { + myFhirContext = theFhirContext; + myDaoRegistry = theDaoRegistry; + } + + + @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED) + public void resourceUpdatedPreCommit(RequestDetails theRequest, IBaseResource theResource) { + ourLog.debug("Validating address on for create {}, {}", theResource, theRequest); + handleRequest(theRequest, theResource); + } + + @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED) + public void resourceCreatedPreCommit(RequestDetails theRequest, IBaseResource theResource) { + ourLog.debug("Validating address on for create {}, {}", theResource, theRequest); + handleRequest(theRequest, theResource); + } + + private void handleRequest(RequestDetails theRequest, IBaseResource theResource) { + switch (myFhirContext.getResourceType(theResource)) { + case "Location": + flattenLocation(theResource); + return; + case "Account": + flattenAccount(theResource); + return; + case "Role": + flattenRole(theResource); + return; + default: + return; + } + } + + private void flattenLocation(IBaseResource theResource) { + TerserUtilHelper helper = TerserUtilHelper.newHelper(myFhirContext, theResource); + IBase managingOrganizationsRef = helper.getFieldValue("managingOrganization"); + if (managingOrganizationsRef == null) { + ourLog.info("No organization is associated with {}", theResource); + return; + } + + IBaseResource referencedOrg = ((IBaseReference) managingOrganizationsRef).getResource(); + if (referencedOrg == null) { + ourLog.warn("Missing value for reference {}", referencedOrg); + return; + } + TerserUtil.getValues(myFhirContext, referencedOrg, "address").clear(); + + IBase newAddress = helper.getFieldValue("address"); + TerserUtil.setFieldByFhirPath(myFhirContext, "address", referencedOrg, newAddress); + + IFhirResourceDao dao = getDao(referencedOrg); + dao.update(referencedOrg); + } + + private void flattenAccount(IBaseResource theAccount) { + TerserUtilHelper account = TerserUtilHelper.newHelper(myFhirContext, theAccount); + List owner = account.getFieldValues("owner"); + if (owner.isEmpty()) { + ourLog.info("No organization is associated with {}", theAccount); + return; + } + + for (IBase o : owner) { + IBaseResource org = ((IBaseReference) o).getResource(); + if (org == null) { + ourLog.warn("Missing value for reference {}", o); + continue; + } + + IBaseExtension ext = ExtensionUtil.getOrCreateExtension(org, "http://hapifhir.org/Flattener/Account"); + IPrimitiveType status = (IPrimitiveType) TerserUtil.getFirstFieldByFhirPath(myFhirContext, "status", theAccount); + ExtensionUtil.setExtension(myFhirContext, ext, status.getValueAsString()); + + IFhirResourceDao dao = getDao(org); + dao.update(org); + } + } + + // PractitionerRole.specialty becomes Practitioner.Extension.valueCode + private void flattenRole(IBaseResource theRole) { + TerserUtilHelper role = TerserUtilHelper.newHelper(myFhirContext, theRole); + + List specialties = role.getFieldValues("specialty"); + if (specialties.isEmpty()) { + ourLog.info("No specialties are associated with {}", theRole); + return; + } + + for (IBase o : specialties) { + IBaseResource practitioner = (IBaseResource) TerserUtil.getValue(myFhirContext, theRole, "practitioner"); + if (practitioner == null) { + ourLog.warn("Practitioner is not set on {}", theRole); + continue; + } + + String roleUrl = "http://hapifhir.org/Flattener/Role/" + theRole.toString(); + + IBaseExtension ext = ExtensionUtil.getOrCreateExtension(practitioner, roleUrl); + ext.setValue(new CodeDt(String.valueOf(role.getFieldValue("status")))); + + IFhirResourceDao dao = getDao(practitioner); + dao.update(practitioner); + } + } + + private IFhirResourceDao getDao(IBaseResource theReferencedOrg) { + String resourceType = myFhirContext.getResourceType(theReferencedOrg); + return myDaoRegistry.getResourceDao(resourceType); + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/ResourceFlatteningInterceptorTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/ResourceFlatteningInterceptorTest.java new file mode 100644 index 00000000000..63758dc3842 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/interceptor/ResourceFlatteningInterceptorTest.java @@ -0,0 +1,124 @@ +package ca.uhn.fhir.jpa.interceptor; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test; +import ca.uhn.fhir.util.TerserUtilHelper; +import org.checkerframework.checker.units.qual.A; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Account; +import org.hl7.fhir.r4.model.Address; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.PractitionerRole; +import org.hl7.fhir.r4.model.Reference; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.annotation.Nonnull; + +import static org.junit.jupiter.api.Assertions.*; + +class ResourceFlatteningInterceptorTest extends BaseResourceProviderR4Test { + + private ResourceFlatteningInterceptor myInterceptor; + + @Autowired + private DaoRegistry myDaoRegistry; + @Autowired + private FhirContext myFhirContext; + + @BeforeEach + public void init() { + myInterceptor = new ResourceFlatteningInterceptor(myFhirContext, myDaoRegistry); + } + + public IFhirResourceDao getDao(IBaseResource theResource) { + String type = myFhirContext.getResourceType(theResource); + return myDaoRegistry.getResourceDao(type); + } + + @Test + public void testFlatteningOfLocation() { + // Location.address becomes Organization.address + Organization newOrganization = newPersistentOrganization(); + + Location location = new Location(); + Address newAddress = new Address(); + newAddress.addLine("NEW LINE 1").setCity("NEW CITY"); + location.setAddress(newAddress); + location.setManagingOrganization(new Reference(newOrganization)); + + myInterceptor.resourceCreatedPreCommit(null, location); + + Organization savedOrganization = (Organization) getDao(newOrganization) + .read(newOrganization.getIdElement()); + assertEquals(1, savedOrganization.getAddress().size(), "Expect old addresses cleared up"); + + Address savedAddress = savedOrganization.getAddressFirstRep(); + assertTrue(newAddress.equalsDeep(savedAddress), "Expect organization address replaced"); + } + + @Nonnull + private Organization newPersistentOrganization() { + Organization newOrganization = newOrganization(); + return (Organization) getDao(newOrganization).create(newOrganization).getResource(); + } + + @Nonnull + private Organization newOrganization() { + Organization retVal = new Organization(); + retVal.addAddress() + .addLine("Address 1 Line 1") + .setCity("Address 1 City"); + retVal.addAddress() + .addLine("Address 2 Line 1") + .setCity("Address 2 City"); + return retVal; + } + + @Test + public void testFlatteningOfLocationWithEmptyManagingOrg() { + try { + myInterceptor.resourceCreatedPreCommit(null, new Location()); + } catch (Exception e) { + fail("Expected no errors"); + } + } + + @Test + public void testFlatteningOfAccount() { + // Account.status become Organization.Extension.valueCode + Organization organization = newPersistentOrganization(); + assertTrue(organization.getExtension().isEmpty()); + + Account account = new Account(); + account.setName("Test Account"); + account.setStatus(Account.AccountStatus.ACTIVE); + account.setDescription("Test Active Account"); + account.setOwner(new Reference(organization)); + + myInterceptor.resourceCreatedPreCommit(null, account); + + Organization savedOrganization = (Organization) getDao(organization).read(organization.getIdElement()); + assertFalse(organization.getExtension().isEmpty()); + + assertEquals(Account.AccountStatus.ACTIVE.toCode(), organization.getExtension().get(0).getValue().primitiveValue()); + } + + @Test + public void testFlatteningOfAnEmptyAccout() { + myInterceptor.resourceCreatedPreCommit(null, new Account()); + } + + public void testFlatteningOfPractitionerRole() { + // PractitionerRole.specialty becomes Practitioner.Extension.valueCode + PractitionerRole role = new PractitionerRole(); + CodeableConcept specialty = role.addSpecialty(); + } + +} diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/TerserUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/TerserUtilTest.java index 52826cfc893..d23945e8ce0 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/TerserUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/TerserUtilTest.java @@ -57,9 +57,11 @@ class TerserUtilTest { assertEquals(1, p2Helper.getFieldValues("identifier").size()); Identifier id1 = (Identifier) p1Helper.getFieldValues("identifier").get(0); - Identifier id2 = (Identifier) p2Helper.getFieldValues("identifier").get(0); + Identifier id2 = (Identifier) p2Helper.getFieldValue("identifier"); assertTrue(id1.equalsDeep(id2)); assertFalse(id1.equals(id2)); + + assertNull(p2Helper.getFieldValue("address")); } @Test