Initial commit

This commit is contained in:
Nick Goupinets 2021-03-19 09:58:15 -04:00
parent c9c13646f5
commit 5747d5d841
6 changed files with 371 additions and 10 deletions

View File

@ -28,6 +28,7 @@ import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions; import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import java.util.List; import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -103,14 +104,44 @@ public class ExtensionUtil {
* @param theExtensionUrl URL of the extension to get. Must be non-null * @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 * @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) { public static IBaseExtension<?, ?> getExtension(IBase theBase, String theExtensionUrl) {
return theBase.getExtension() return validateExtensionSupport(theBase)
.getExtension()
.stream() .stream()
.filter(e -> theExtensionUrl.equals(e.getUrl())) .filter(e -> theExtensionUrl.equals(e.getUrl()))
.findFirst() .findFirst()
.orElse(null); .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<IBaseExtension<?, ?>> getExtensions(IBase theBase, Predicate<? super IBaseExtension> 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<IBaseExtension<?, ?>> clearExtensions(IBase theBase, Predicate<? super IBaseExtension> theFilter) {
List<IBaseExtension<?, ?>> retVal = getExtensions(theBase, theFilter);
validateExtensionSupport(theBase)
.getExtension()
.removeIf(theFilter);
return retVal;
}
/** /**
* Gets all extensions with the specified URL * 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 * @return Returns all extension with the specified URL, or an empty list if such extensions do not exist
*/ */
public static List<IBaseExtension<?, ?>> getExtensions(IBaseHasExtensions theBase, String theExtensionUrl) { public static List<IBaseExtension<?, ?>> getExtensions(IBaseHasExtensions theBase, String theExtensionUrl) {
return theBase.getExtension() Predicate<IBaseExtension> urlEqualityPredicate = e -> theExtensionUrl.equals(e.getUrl());
.stream() return getExtensions(theBase, urlEqualityPredicate);
.filter(e -> theExtensionUrl.equals(e.getUrl()))
.collect(Collectors.toList());
} }
/** /**

View File

@ -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 theFhirContext Context holding resource definition
* @param theResource Resource to check if the specified field is set * @param theResource Resource to check if the specified field is set
* @param theFieldName name of the field to check * @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<IBase> getValues(FhirContext theFhirContext, IBaseResource theResource, String theFieldName) { public static List<IBase> getValues(FhirContext theFhirContext, IBaseResource theResource, String theFieldName) {
RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResource); RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResource);
@ -114,6 +114,23 @@ public final class TerserUtil {
return resourceIdentifier.getAccessor().getValues(theResource); 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<IBase> 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 * Clones specified composite field (collection). Composite field values must conform to the collections
* contract. * contract.
@ -256,6 +273,20 @@ public final class TerserUtil {
childDefinition.getAccessor().getValues(theResource).clear(); 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 * 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...)} * 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); setFieldByFhirPath(theFhirContext.newTerser(), theFhirPath, theResource, theValue);
} }
public static List<IBase> 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<IBase> 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) { private static void replaceField(IBaseResource theFrom, IBaseResource theTo, BaseRuntimeChildDefinition childDefinition) {
childDefinition.getAccessor().getFirstValueOrNull(theFrom).ifPresent(v -> { childDefinition.getAccessor().getFirstValueOrNull(theFrom).ifPresent(v -> {
childDefinition.getMutator().setValue(theTo, v); childDefinition.getMutator().setValue(theTo, v);

View File

@ -111,12 +111,27 @@ public class TerserUtilHelper {
* Gets values of the specified field. * Gets values of the specified field.
* *
* @param theField The field to get values from * @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<IBase> getFieldValues(String theField) { public List<IBase> getFieldValues(String theField) {
return TerserUtil.getValues(myContext, myResource, 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<IBase> values = getFieldValues(theField);
if (values == null || values.isEmpty()) {
return null;
}
return values.get(0);
}
/** /**
* Gets the terser instance, creating one if necessary. * Gets the terser instance, creating one if necessary.
* *

View File

@ -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<IBase> 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<IBase> 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);
}
}

View File

@ -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 <T extends IBaseResource> IFhirResourceDao<T> 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();
}
}

View File

@ -57,9 +57,11 @@ class TerserUtilTest {
assertEquals(1, p2Helper.getFieldValues("identifier").size()); assertEquals(1, p2Helper.getFieldValues("identifier").size());
Identifier id1 = (Identifier) p1Helper.getFieldValues("identifier").get(0); 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)); assertTrue(id1.equalsDeep(id2));
assertFalse(id1.equals(id2)); assertFalse(id1.equals(id2));
assertNull(p2Helper.getFieldValue("address"));
} }
@Test @Test