Merge pull request #2587 from hapifhir/issue-2585-enhance-placeholder-targets-logic

Issue 2585 enhance placeholder targets logic
This commit is contained in:
Tadgh 2021-04-30 12:55:27 -04:00 committed by GitHub
commit 983d811af6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 351 additions and 19 deletions

View File

@ -0,0 +1,5 @@
---
type: add
issue: 2585
title: "When automatically creating a placeholder reference that is set to auto-populate identifiers, logic has been improved. If the reference does not contain an
identifier, but the inline match URL does, the identifier found in the match URL will be added to the target. If both are populated, they will both be added to the target."

View File

@ -33,17 +33,20 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
import ca.uhn.fhir.mdm.util.CanonicalIdentifier;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.TerserUtil;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull;
@ -122,11 +125,7 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
@SuppressWarnings("unchecked")
T newResource = (T) missingResourceDef.newInstance();
if (newResource instanceof IBaseHasExtensions) {
IBaseExtension<?, ?> extension = ((IBaseHasExtensions) newResource).addExtension();
extension.setUrl(HapiExtensions.EXT_RESOURCE_PLACEHOLDER);
extension.setValue(myContext.getPrimitiveBoolean(true));
}
tryToAddPlaceholderExtensionToResource(newResource);
IFhirResourceDao<T> placeholderResourceDao = myDaoRegistry.getResourceDao(theType);
ourLog.debug("Automatically creating empty placeholder resource: {}", newResource.getIdElement().getValue());
@ -146,26 +145,98 @@ public class DaoResourceLinkResolver implements IResourceLinkResolver {
return Optional.ofNullable(valueOf);
}
private <T extends IBaseResource> void tryToAddPlaceholderExtensionToResource(T newResource) {
if (newResource instanceof IBaseHasExtensions) {
IBaseExtension<?, ?> extension = ((IBaseHasExtensions) newResource).addExtension();
extension.setUrl(HapiExtensions.EXT_RESOURCE_PLACEHOLDER);
extension.setValue(myContext.getPrimitiveBoolean(true));
}
}
private <T extends IBaseResource> void tryToCopyIdentifierFromReferenceToTargetResource(IBaseReference theSourceReference, RuntimeResourceDefinition theTargetResourceDef, T theTargetResource) {
boolean referenceHasIdentifier = theSourceReference.hasIdentifier();
if (referenceHasIdentifier) {
BaseRuntimeChildDefinition targetIdentifier = theTargetResourceDef.getChildByName("identifier");
if (targetIdentifier != null) {
BaseRuntimeElementDefinition<?> identifierElement = targetIdentifier.getChildByName("identifier");
String identifierElementName = identifierElement.getName();
boolean targetHasIdentifierElement = identifierElementName.equals("Identifier");
if (targetHasIdentifierElement) {
// boolean referenceHasIdentifier = theSourceReference.hasIdentifier();
CanonicalIdentifier referenceMatchUrlIdentifier = extractIdentifierFromUrl(theSourceReference.getReferenceElement().getValue());
CanonicalIdentifier referenceIdentifier = extractIdentifierReference(theSourceReference);
BaseRuntimeElementCompositeDefinition<?> referenceElement = (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theSourceReference.getClass());
BaseRuntimeChildDefinition referenceIdentifierChild = referenceElement.getChildByName("identifier");
Optional<IBase> identifierOpt = referenceIdentifierChild.getAccessor().getFirstValueOrNull(theSourceReference);
identifierOpt.ifPresent(theIBase -> targetIdentifier.getMutator().addValue(theTargetResource, theIBase));
}
if (referenceIdentifier == null && referenceMatchUrlIdentifier != null) {
addMatchUrlIdentifierToTargetResource(theTargetResourceDef, theTargetResource, referenceMatchUrlIdentifier);
} else if (referenceIdentifier!= null && referenceMatchUrlIdentifier == null) {
addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource);
} else if (referenceIdentifier != null && referenceMatchUrlIdentifier != null) {
if (referenceIdentifier.equals(referenceMatchUrlIdentifier)) {
addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource);
} else {
addSubjectIdentifierToTargetResource(theSourceReference, theTargetResourceDef, theTargetResource);
addMatchUrlIdentifierToTargetResource(theTargetResourceDef, theTargetResource, referenceMatchUrlIdentifier);
}
}
}
private <T extends IBaseResource> void addSubjectIdentifierToTargetResource(IBaseReference theSourceReference, RuntimeResourceDefinition theTargetResourceDef, T theTargetResource) {
BaseRuntimeChildDefinition targetIdentifier = theTargetResourceDef.getChildByName("identifier");
if (targetIdentifier != null) {
BaseRuntimeElementDefinition<?> identifierElement = targetIdentifier.getChildByName("identifier");
String identifierElementName = identifierElement.getName();
boolean targetHasIdentifierElement = identifierElementName.equals("Identifier");
if (targetHasIdentifierElement) {
BaseRuntimeElementCompositeDefinition<?> referenceElement = (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(theSourceReference.getClass());
BaseRuntimeChildDefinition referenceIdentifierChild = referenceElement.getChildByName("identifier");
Optional<IBase> identifierOpt = referenceIdentifierChild.getAccessor().getFirstValueOrNull(theSourceReference);
identifierOpt.ifPresent(theIBase -> targetIdentifier.getMutator().addValue(theTargetResource, theIBase));
}
}
}
private <T extends IBaseResource> void addMatchUrlIdentifierToTargetResource(RuntimeResourceDefinition theTargetResourceDef, T theTargetResource, CanonicalIdentifier referenceMatchUrlIdentifier) {
BaseRuntimeChildDefinition identifierDefinition = theTargetResourceDef.getChildByName("identifier");
IBase identifierIBase = identifierDefinition.getChildByName("identifier").newInstance(identifierDefinition.getInstanceConstructorArguments());
IBase systemIBase = TerserUtil.newElement(myContext, "uri", referenceMatchUrlIdentifier.getSystemElement().getValueAsString());
IBase valueIBase = TerserUtil.newElement(myContext, "string", referenceMatchUrlIdentifier.getValueElement().getValueAsString());
//Set system in the IBase Identifier
BaseRuntimeElementDefinition<?> elementDefinition = myContext.getElementDefinition(identifierIBase.getClass());
BaseRuntimeChildDefinition systemDefinition = elementDefinition.getChildByName("system");
systemDefinition.getMutator().setValue(identifierIBase, systemIBase);
BaseRuntimeChildDefinition valueDefinition = elementDefinition.getChildByName("value");
valueDefinition.getMutator().setValue(identifierIBase, valueIBase);
//Set Value in the IBase identifier
identifierDefinition.getMutator().addValue(theTargetResource, identifierIBase);
}
private CanonicalIdentifier extractIdentifierReference(IBaseReference theSourceReference) {
Optional<IBase> identifier = myContext.newFhirPath().evaluateFirst(theSourceReference, "identifier", IBase.class);
if (!identifier.isPresent()) {
return null;
} else {
CanonicalIdentifier canonicalIdentifier = new CanonicalIdentifier();
Optional<IPrimitiveType> system = myContext.newFhirPath().evaluateFirst(identifier.get(), "system", IPrimitiveType.class);
Optional<IPrimitiveType> value = myContext.newFhirPath().evaluateFirst(identifier.get(), "value", IPrimitiveType.class);
system.ifPresent(theIPrimitiveType -> canonicalIdentifier.setSystem(theIPrimitiveType.getValueAsString()));
value.ifPresent(theIPrimitiveType -> canonicalIdentifier.setValue(theIPrimitiveType.getValueAsString()));
return canonicalIdentifier;
}
}
private CanonicalIdentifier extractIdentifierFromUrl(String theValue) {
if (!theValue.contains("identifier=")) {
return null;
}
CanonicalIdentifier identifier = new CanonicalIdentifier();
String identifierString = theValue.substring(theValue.indexOf("=") + 1);
String[] split = identifierString.split("\\|");
if (split.length != 2) {
throw new IllegalArgumentException("Can't create a placeholder reference with identifier " + theValue + ". It is not a valid identifier");
}
identifier.setSystem(split[0]);
identifier.setValue(split[1]);
return identifier;
}
@Override
public void validateTypeOrThrowException(Class<? extends IBaseResource> theType) {
myDaoRegistry.getDaoOrThrowException(theType);

View File

@ -276,6 +276,7 @@ public class SearchParamWithInlineReferencesExtractor {
throw new InvalidRequestException(msg);
}
Class<? extends IBaseResource> matchResourceType = matchResourceDef.getImplementingClass();
//Attempt to find the target reference before creating a placeholder
Set<ResourcePersistentId> matches = myMatchResourceUrlService.processMatchUrl(nextIdText, matchResourceType, theRequest);
ResourcePersistentId match;

View File

@ -14,6 +14,7 @@ import org.hl7.fhir.r4.model.AuditEvent;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Observation.ObservationStatus;
import org.hl7.fhir.r4.model.Patient;
@ -24,9 +25,11 @@ import org.junit.jupiter.api.Test;
import java.util.List;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -241,6 +244,216 @@ public class FhirResourceDaoCreatePlaceholdersR4Test extends BaseJpaR4Test {
assertEquals(0, patient.getIdentifier().size());
}
// Case 1:
//
// IF the inline match URL does include an identifier
// AND the reference does not include an identifier
// AND a placeholder reference target is to be created
// DO use the value of the inline match URL's identifier to populate an identifier in the placeholder
@Test
public void testCreatePlaceholderWithMatchUrl_NoReferenceDefined() {
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
myDaoConfig.setAllowInlineMatchUrlReferences(true);
myDaoConfig.setPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets(true);
/*
* Create an Observation that references a Patient
* Reference is populated with inline match URL and includes identifier which differs from the inlined identifier
*/
Observation obsToCreate = new Observation();
obsToCreate.setStatus(ObservationStatus.FINAL);
obsToCreate.getSubject().setReference("Patient?identifier=http://foo|123");
IIdType obsId = myObservationDao.create(obsToCreate, mySrd).getId();
// Read the Observation
Observation createdObs = myObservationDao.read(obsId);
//Read the Placeholder Patient
Patient placeholderPat = myPatientDao.read(new IdType(createdObs.getSubject().getReference()));
ourLog.info("\nObservation created:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdObs));
//Ensure the Obs has the right placeholder ID.
IIdType placeholderPatId = placeholderPat.getIdElement();
assertEquals(createdObs.getSubject().getReference(), placeholderPatId.toUnqualifiedVersionless().getValueAsString());
/*
* Should have a single identifier populated.
*/
ourLog.info("\nPlaceholder Patient created:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(placeholderPat));
assertEquals(1, placeholderPat.getIdentifier().size());
List<Identifier> identifiers = placeholderPat.getIdentifier();
Identifier identifier = identifiers.get(0);
assertThat(identifier.getSystem(), is(equalTo("http://foo")));
assertThat(identifier.getValue(), is(equalTo("123")));
}
// Case 2:
//
// IF the inline match URL does not include an identifier
// AND the reference does include an identifier
// AND a placeholder reference target is to be created
// DO use the value of the reference's identifier to populate an identifier in the placeholder
@Test
public void testCreatePlaceholderReferenceWhereInlineMatchUrlDoesNotContainIdentifierButSubjectReferenceDoes() {
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
myDaoConfig.setAllowInlineMatchUrlReferences(true);
myDaoConfig.setPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets(true);
/*
* Create an Observation that references a Patient
* Reference is populated with inline match URL and includes identifier which differs from the inlined identifier
*/
Observation obsToCreate = new Observation();
obsToCreate.setStatus(ObservationStatus.FINAL);
obsToCreate.getSubject().setReference("Patient?name=Johhnybravo");
obsToCreate.getSubject().getIdentifier().setSystem("http://foo").setValue("123");
IIdType obsId = myObservationDao.create(obsToCreate, mySrd).getId();
// Read the Observation
Observation createdObs = myObservationDao.read(obsId);
//Read the Placeholder Patient
Patient placeholderPat = myPatientDao.read(new IdType(createdObs.getSubject().getReference()));
ourLog.info("\nObservation created:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdObs));
//Ensure the Obs has the right placeholder ID.
IIdType placeholderPatId = placeholderPat.getIdElement();
assertEquals(createdObs.getSubject().getReference(), placeholderPatId.toUnqualifiedVersionless().getValueAsString());
/*
* Should have a single identifier populated.
*/
ourLog.info("\nPlaceholder Patient created:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(placeholderPat));
assertEquals(1, placeholderPat.getIdentifier().size());
List<Identifier> identifiers = placeholderPat.getIdentifier();
Identifier identifier = identifiers.get(0);
assertThat(identifier.getSystem(), is(equalTo("http://foo")));
assertThat(identifier.getValue(), is(equalTo("123")));
}
// Case 3:
//
// IF the inline match URL does include an identifier
// AND the reference does include an identifier
// AND the identifiers are the same
// AND a placeholder reference target is to be created
// DO use only the value of the reference's identifier to populate an identifier in the placeholder
@Test
public void testCreatePlaceholderWithMatchingInlineAndSubjectReferenceIdentifiersCreatesOnlyOne() {
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
myDaoConfig.setAllowInlineMatchUrlReferences(true);
myDaoConfig.setPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets(true);
/*
* Create an Observation that references a Patient
* Reference is populated with inline match URL and includes identifier which differs from the inlined identifier
*/
Observation obsToCreate = new Observation();
obsToCreate.setStatus(ObservationStatus.FINAL);
obsToCreate.getSubject().setReference("Patient?identifier=http://bar|321");
obsToCreate.getSubject().getIdentifier().setSystem("http://bar").setValue("321");
IIdType obsId = myObservationDao.create(obsToCreate, mySrd).getId();
// Read the Observation
Observation createdObs = myObservationDao.read(obsId);
//Read the Placeholder Patient
Patient placeholderPat = myPatientDao.read(new IdType(createdObs.getSubject().getReference()));
ourLog.info("\nObservation created:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdObs));
//Ensure the Obs has the right placeholder ID.
IIdType placeholderPatId = placeholderPat.getIdElement();
assertEquals(createdObs.getSubject().getReference(), placeholderPatId.toUnqualifiedVersionless().getValueAsString());
/*
* Should have a single identifier populated.
*/
ourLog.info("\nPlaceholder Patient created:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(placeholderPat));
assertEquals(1, placeholderPat.getIdentifier().size());
List<Identifier> identifiers = placeholderPat.getIdentifier();
Identifier identifier = identifiers.get(0);
assertThat(identifier.getSystem(), is(equalTo("http://bar")));
assertThat(identifier.getValue(), is(equalTo("321")));
}
// Case 4:
//
// IF the inline match URL does include an identifier
// AND the reference does include an identifier
// AND the identifiers are different
// AND a placeholder reference target is to be created
// DO use both the value of the inline match URL's identifier and the value of the reference's identifier to populate two identifiers in the placeholder
@Test
public void testCreatePlaceholderWithMisMatchedIdentifiers_BothIdentifiersCopied() {
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);
myDaoConfig.setAllowInlineMatchUrlReferences(true);
myDaoConfig.setPopulateIdentifierInAutoCreatedPlaceholderReferenceTargets(true);
/*
* Create an Observation that references a Patient
* Reference is populated with inline match URL and includes identifier which differs from the inlined identifier
*/
Observation obsToCreate = new Observation();
obsToCreate.setStatus(ObservationStatus.FINAL);
obsToCreate.getSubject().setReference("Patient?identifier=http://foo|123");
obsToCreate.getSubject().getIdentifier().setSystem("http://bar").setValue("321");
IIdType obsId = myObservationDao.create(obsToCreate, mySrd).getId();
// Read the Observation
Observation createdObs = myObservationDao.read(obsId);
//Read the Placeholder Patient
Patient placeholderPat = myPatientDao.read(new IdType(createdObs.getSubject().getReference()));
ourLog.info("\nObservation created:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdObs));
//Ensure the Obs has the right placeholder ID.
IIdType placeholderPatId = placeholderPat.getIdElement();
assertEquals(createdObs.getSubject().getReference(), placeholderPatId.toUnqualifiedVersionless().getValueAsString());
/*
* Placeholder Identifiers should both be populated since they were both provided, and did not match
*/
ourLog.info("\nPlaceholder Patient created:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(placeholderPat));
assertEquals(2, placeholderPat.getIdentifier().size());
List<Identifier> identifiers = placeholderPat.getIdentifier();
//inline match-url identifier
assertThat(identifiers.get(1).getSystem(), is(equalTo("http://foo")));
assertThat(identifiers.get(1).getValue(), is(equalTo("123")));
//subject identifier
assertThat(identifiers.get(0).getSystem(), is(equalTo("http://bar")));
assertThat(identifiers.get(0).getValue(), is(equalTo("321")));
// Conditionally update a Patient with the same identifier
Patient patToConditionalUpdate = new Patient();
patToConditionalUpdate.addIdentifier().setSystem("http://foo").setValue("123");
patToConditionalUpdate.addName().setFamily("Simpson");
IIdType conditionalUpdatePatId = myPatientDao.update(patToConditionalUpdate, "Patient?identifier=http://foo|123", mySrd).getId();
// Read the conditionally updated Patient
Patient conditionalUpdatePat = myPatientDao.read(conditionalUpdatePatId);
ourLog.info("\nConditionally updated Patient:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conditionalUpdatePat));
assertEquals(1, conditionalUpdatePat.getIdentifier().size());
/*
* Observation should reference conditionally updated Patient
* ID of placeholder Patient should match ID of conditionally updated Patient
*/
createdObs = myObservationDao.read(obsId);
ourLog.info("\nObservation read after Patient update:\n" + myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createdObs));
assertEquals(createdObs.getSubject().getReference(), conditionalUpdatePatId.toUnqualifiedVersionless().getValueAsString());
assertEquals(placeholderPatId.toUnqualifiedVersionless().getValueAsString(), conditionalUpdatePatId.toUnqualifiedVersionless().getValueAsString());
}
@Test
public void testCreatePlaceholderWithMatchUrl_IdentifierCopiedByDefault_WithUpdateToTarget() {
myDaoConfig.setAutoCreatePlaceholderReferenceTargets(true);

View File

@ -49,6 +49,8 @@ public final class IdentifierUtil {
return retval;
}
/**
* Retrieves appropriate FHIR Identifier model instance based on the context version
*

40
pom.xml
View File

@ -2800,5 +2800,45 @@
</plugins>
</build>
</profile>
<profile>
<id>FASTINSTALL</id>
<properties>
<maven.test.skip>true</maven.test.skip>
</properties>
<!-- Profile for a quick local mvn install after a git pull.
We assume upstream ran these checks as part of the build. -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution><id>integration-test</id><phase>none</phase></execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<executions>
<execution><id>validate</id><phase>none</phase></execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
<executions>
<execution><id>check-java-api</id><phase>none</phase></execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>