MDM Virtualization Interceptor (#6464)

* Work on virtualization

* Fixes

* Cleanup

* Fixes

* MDM virtualization

* Remove fixme

* Test cleanup

* Cleanup

* Version bump

* Fixes

* Cleanup

* Fixes

* WIP

* Test cleanup

* Spotless

* Checkstyle

* Spotless

* Address review comments

* Test fix

* Test fix

* Address review comments

* Version bump

* Version bump

* License header
This commit is contained in:
James Agnew 2024-11-21 11:01:54 -05:00 committed by GitHub
parent e182694608
commit dea9706652
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
117 changed files with 1389 additions and 319 deletions

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -63,6 +63,17 @@ public interface IFhirVersion {
IIdType newIdType();
/**
* Creates a new {@link IIdType} instance for the given version with the given value
*
* @since 8.0.0
*/
default IIdType newIdType(String theValue) {
IIdType retVal = newIdType();
retVal.setValue(theValue);
return retVal;
}
/**
* Returns an instance of <code>IFhirVersionServer<code> for this version.
* Note that this method may only be called if the <code>hapi-fhir-server</code>

View File

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<packaging>pom</packaging>
<name>HAPI FHIR BOM</name>
@ -12,7 +12,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -330,7 +330,7 @@ public class AuthorizationInterceptors {
SearchNarrowingConsentService consentService =
new SearchNarrowingConsentService(validationSupport, searchParamRegistry);
// Create a ConsentIntereptor to apply the ConsentService and register it with the server
// Create a ConsentInterceptor to apply the ConsentService and register it with the server
ConsentInterceptor consentInterceptor = new ConsentInterceptor();
consentInterceptor.registerConsentService(consentService);
restfulServer.registerInterceptor(consentInterceptor);

View File

@ -7,7 +7,7 @@
title: "Remove a dependency on a Java 1.7 class (ReflectiveOperationException) in several spots in the codebase. This dependency was accidentally introduced in 1.3, and animal-sniffer-plugin failed to detect it (sigh)."
- item:
type: "add"
title: "Add two new server interceptors: RequestValidatingInterceptor and ResponseValidatingInterceptor which can be used to validate incoming requests or outgoing responses using the standard FHIR validation tools. See the Server Validation Page for examples of how to use these interceptors. These intereptors have both been enabled on the <a href=\"http://fhirtest.uhn.ca\">public test page</a>."
title: "Add two new server interceptors: RequestValidatingInterceptor and ResponseValidatingInterceptor which can be used to validate incoming requests or outgoing responses using the standard FHIR validation tools. See the Server Validation Page for examples of how to use these interceptors. These interceptors have both been enabled on the <a href=\"http://fhirtest.uhn.ca\">public test page</a>."
- item:
issue: "259"
type: "fix"

View File

@ -32,7 +32,7 @@
title: "<b>New Feature</b>: The JPA server now supports the <code>_filter</code> search parameter when configured to do so. The <a href=\"http://hl7.org/fhir/search_filter.html\">filter search parameter</a> is an extremely flexible and powerful feature, allowing for advanced grouping and order of operations on searches. It can be dangerous however, as it potentially allows users to create queries for which no database indexes exist in the default configuration so it is disabled by default. Thanks to Anthony Sute for the pull request and all of his support in what turned out to be a lengthy merge!"
- item:
type: "add"
title: "<b>New Feature</b>: A new interceptor called CascadingDeleteInterceptor has been added to the JPA project. This interceptor allows deletes to cascade when a specific URL parameter or header is added to the request. Cascading deletes can also be controlled by a new flag in the AuthorizationIntereptor RuleBuilder, in order to ensure that cascading deletes are only available to users with sufficient permission."
title: "<b>New Feature</b>: A new interceptor called CascadingDeleteInterceptor has been added to the JPA project. This interceptor allows deletes to cascade when a specific URL parameter or header is added to the request. Cascading deletes can also be controlled by a new flag in the AuthorizationInterceptor RuleBuilder, in order to ensure that cascading deletes are only available to users with sufficient permission."
- item:
type: "add"
title: "Several enhancements have been made to the <code>AuthorizationInterceptor</code> : <ul> <li>The interceptor now registers against the <code>STORAGE_PRESHOW_RESOURCES</code> interceptor hook, which allows it to successfully authorize JPA operations that don't actually return resource content, such as GraphQL responses, and resources that have been filtered using the <code>_elements</code> parameter.</li> <li> </li>The rule list is now cached on a per-request basis, which should improve performance</ul>"

View File

@ -0,0 +1,8 @@
---
type: add
issue: 6464
title: "A new experimental interceptor called the MdmReadVirtualizationInterceptor
has been added. This interceptor rewrites results when querying an MDM-enabled
JPA server in order to always include linked resources and rerwrites query results
to link to the MDM golden resource. This interceptor is still being developed
and should be used with caution."

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -29,6 +29,7 @@ import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.mdm.dao.IMdmLinkDao;
import ca.uhn.fhir.mdm.dao.IMdmLinkImplFactory;
import ca.uhn.fhir.mdm.svc.MdmLinkExpandSvc;
import ca.uhn.fhir.mdm.svc.MdmSearchExpansionSvc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -40,6 +41,11 @@ public class MdmJpaConfig {
return new MdmLinkExpandSvc();
}
@Bean
public MdmSearchExpansionSvc mdmSearchExpansionSvc() {
return new MdmSearchExpansionSvc();
}
@Bean
public IMdmLinkDao<JpaPid, MdmLink> mdmLinkDao() {
return new MdmLinkDaoJpaImpl();

View File

@ -1020,6 +1020,10 @@ public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStora
if (entity.getId() == null) {
myEntityManager.persist(entity);
if (entity.getFhirId() == null) {
entity.setFhirId(Long.toString(entity.getResourceId()));
}
postPersist(entity, (T) theResource, theRequest);
} else if (entity.getDeleted() != null) {

View File

@ -172,8 +172,9 @@ public class HistoryBuilder {
// For that reason, strip the prefix before setting the transientForcedId below.
// If not stripped this messes up the id of the resource as the resourceType would be repeated
// twice like Patient/Patient/1234 in the resource constructed
if (resourceId.startsWith(myResourceType + "/")) {
resourceId = resourceId.substring(myResourceType.length() + 1);
int slashIdx = resourceId.indexOf('/');
if (slashIdx != -1) {
resourceId = resourceId.substring(slashIdx + 1);
}
} else {
resourceId = nextResourceId.toString();

View File

@ -29,6 +29,7 @@ import org.springframework.data.repository.history.RevisionRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@ -122,6 +123,20 @@ public interface IMdmLinkJpaRepository
List<MdmPidTuple> expandPidsByGoldenResourcePidAndMatchResult(
@Param("goldenPid") Long theSourcePid, @Param("matchResult") MdmMatchResultEnum theMdmMatchResultEnum);
@Query(
"SELECT lookup_link.myGoldenResourcePid as goldenPid, gld_rt.myPartitionIdValue as goldenPartitionId, lookup_link.mySourcePid as sourcePid, lookup_link.myPartitionIdValue as sourcePartitionId "
+ "FROM MdmLink lookup_link "
+ "INNER JOIN ResourceTable gld_rt "
+ "on lookup_link.myGoldenResourcePid=gld_rt.myId "
+ "WHERE "
+ " (lookup_link.myGoldenResourcePid IN (:pids) "
+ " OR"
+ " lookup_link.mySourcePid IN (:pids))"
+ "AND lookup_link.myMatchResult = :matchResult")
List<MdmPidTuple> expandPidsByGoldenResourcePidsOrSourcePidsAndMatchResult(
@Param("pids") Collection<Long> theSourcePid,
@Param("matchResult") MdmMatchResultEnum theMdmMatchResultEnum);
@Query(
"SELECT ml.myId FROM MdmLink ml WHERE ml.myMdmSourceType = :resourceName AND ml.myCreated <= :highThreshold ORDER BY ml.myCreated DESC")
List<Long> findPidByResourceNameAndThreshold(

View File

@ -69,6 +69,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.history.Revisions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@ -150,6 +151,17 @@ public class MdmLinkDaoJpaImpl implements IMdmLinkDao<JpaPid, MdmLink> {
.collect(Collectors.toList());
}
@Override
public Collection<MdmPidTuple<JpaPid>> resolveGoldenResources(List<JpaPid> theSourcePids) {
return myMdmLinkDao
.expandPidsByGoldenResourcePidsOrSourcePidsAndMatchResult(
JpaPid.toLongList(theSourcePids), MdmMatchResultEnum.MATCH)
.stream()
.map(this::daoTupleToMdmTuple)
.distinct()
.collect(Collectors.toList());
}
@Override
public List<JpaPid> findPidByResourceNameAndThreshold(
String theResourceName, Date theHighThreshold, Pageable thePageable) {

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -3,7 +3,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -3,7 +3,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.mdm.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.nickname.INicknameSvc;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.interceptor.MdmReadVirtualizationInterceptor;
import ca.uhn.fhir.mdm.interceptor.MdmSearchExpandingInterceptor;
import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator;
import ca.uhn.fhir.mdm.rules.matcher.IMatcherFactory;
@ -46,6 +47,12 @@ public class MdmCommonConfig {
return new MdmSearchExpandingInterceptor();
}
@Bean
@Lazy
public MdmReadVirtualizationInterceptor<?> mdmReadVirtualizationInterceptor() {
return new MdmReadVirtualizationInterceptor<>();
}
@Bean
MdmLinkDeleteSvc mdmLinkDeleteSvc() {
return new MdmLinkDeleteSvc();

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
import ca.uhn.fhir.jpa.entity.MdmLink;
@ -92,7 +93,7 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test {
private static final String NAME_GIVEN_FRANK = "Frank";
@Autowired
protected IFhirResourceDao<Patient> myPatientDao;
protected IFhirResourceDaoPatient<Patient> myPatientDao;
@Autowired
protected IFhirResourceDao<Organization> myOrganizationDao;
@Autowired
@ -542,14 +543,6 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test {
return mdmLink;
}
protected void logAllLinks() {
ourLog.info("Logging all MDM Links:");
List<MdmLink> links = myMdmLinkDao.findAll();
for (MdmLink link : links) {
ourLog.info(link.toString());
}
}
protected void assertLinksMatchResult(MdmMatchResultEnum... theExpectedValues) {
assertFields(MdmLink::getMatchResult, theExpectedValues);
}

View File

@ -1,11 +1,17 @@
package ca.uhn.fhir.jpa.mdm.config;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.jpa.mdm.helper.MdmLinkHelper;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.dao.IMdmLinkDao;
import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator;
import ca.uhn.fhir.mdm.rules.config.MdmSettings;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.r4.model.Patient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -45,7 +51,7 @@ public abstract class BaseTestMdmConfig {
}
@Bean
MdmLinkHelper mdmLinkHelper() {
return new MdmLinkHelper();
MdmLinkHelper mdmLinkHelper(IMdmLinkDao<JpaPid, MdmLink> theMdmLinkRepo, IFhirResourceDao<Patient> thePatientDao, MdmLinkDaoSvc<JpaPid, MdmLink> theMdmLinkDaoSvc) {
return new MdmLinkHelper(theMdmLinkRepo, thePatientDao, theMdmLinkDaoSvc);
}
}

View File

@ -37,6 +37,18 @@ public class MdmHelperR4 extends BaseMdmHelper {
return new OutcomeAndLogMessageWrapper(daoMethodOutcome, hookParams);
}
/**
* Invokes {@link #createWithLatch(IBaseResource)} if the supplied resource has no ID set,
* or {@link #updateWithLatch(IBaseResource)} if it does.
*/
public OutcomeAndLogMessageWrapper createOrUpdateWithLatch(IBaseResource theResource) throws InterruptedException {
if (theResource.getIdElement().hasIdPart()) {
return updateWithLatch(theResource);
} else {
return createWithLatch(theResource);
}
}
public OutcomeAndLogMessageWrapper updateWithLatch(IBaseResource theIBaseResource) throws InterruptedException {
return updateWithLatch(theIBaseResource, true);
}
@ -68,6 +80,7 @@ public class MdmHelperR4 extends BaseMdmHelper {
patient.getMeta().addTag(SYSTEM_GOLDEN_RECORD_STATUS, CODE_GOLDEN_RECORD, "Golden Record");
return patient;
}
/**
* OutcomeAndLogMessageWrapper is a simple wrapper class which is _excellent_. It allows us to skip the fact that java doesn't allow
* multiple returns, and wraps both the Method Outcome of the DAO, _and_ the TransactionLogMessages that were passed to the pointcut

View File

@ -2,7 +2,6 @@ package ca.uhn.fhir.jpa.mdm.helper;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.jpa.mdm.helper.testmodels.MDMLinkResults;
@ -21,20 +20,22 @@ import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Patient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.assertj.core.api.Assertions.assertThat;
@Service
public class MdmLinkHelper {
public static final String SERVER_ASSIGNED_PREFIX = "SERVER_ASSIGNED_";
private static final Logger ourLog = LoggerFactory.getLogger(MdmLinkHelper.class);
private enum Side {
@ -42,17 +43,18 @@ public class MdmLinkHelper {
RHS // right hand side; practically speaking, this is the SourceResource of the link
}
@Autowired
private IMdmLinkDao myMdmLinkRepo;
@Autowired
private IFhirResourceDao<Patient> myPatientDao;
@Autowired
private MdmLinkDaoSvc<JpaPid, MdmLink> myMdmLinkDaoSvc;
@SuppressWarnings("rawtypes")
@Autowired
private IMdmLinkDao<JpaPid, MdmLink> myMdmLinkDao;
@Autowired
private IdHelperService myIdHelperService;
private final IMdmLinkDao<JpaPid, MdmLink> myMdmLinkRepo;
private final IFhirResourceDao<Patient> myPatientDao;
private final MdmLinkDaoSvc<JpaPid, MdmLink> myMdmLinkDaoSvc;
/**
* Constructor
*/
public MdmLinkHelper(IMdmLinkDao<JpaPid, MdmLink> theMdmLinkRepo, IFhirResourceDao<Patient> thePatientDao, MdmLinkDaoSvc<JpaPid, MdmLink> theMdmLinkDaoSvc) {
myMdmLinkRepo = theMdmLinkRepo;
myPatientDao = thePatientDao;
myMdmLinkDaoSvc = theMdmLinkDaoSvc;
}
@Transactional
public void logMdmLinks() {
@ -91,7 +93,7 @@ public class MdmLinkHelper {
}
// create all the links
for (MdmTestLinkExpression inputExpression : theState.getParsedInputState()) {
for (MdmTestLinkExpression inputExpression : inputExpressions) {
ourLog.info(inputExpression.getLinkExpression());
Patient goldenResource = theState.getParameter(inputExpression.getLeftSideResourceIdentifier());
@ -106,7 +108,7 @@ public class MdmLinkHelper {
);
matchOutcome.setMatchResultEnum(matchResultType);
MdmLink link = (MdmLink) myMdmLinkDaoSvc.createOrUpdateLinkEntity(
MdmLink link = myMdmLinkDaoSvc.createOrUpdateLinkEntity(
goldenResource, // golden
targetResource, // source
matchOutcome, // match outcome
@ -130,13 +132,21 @@ public class MdmLinkHelper {
}
private Patient createPatientAndTags(String theId, MDMState<Patient, JpaPid> theState) {
boolean serverAssignedId = theId.startsWith(SERVER_ASSIGNED_PREFIX);
boolean previouslyExisting = false;
Patient patient = new Patient();
patient.setActive(true); // all mdm patients must be active
// we add an identifier and use a forced id
// to make test debugging a little simpler
patient.addIdentifier(new Identifier().setValue(theId));
patient.setId(theId);
if (serverAssignedId && theState.getForcedIdForConditionalIdPlaceholder(theId) != null) {
patient.setId(theState.getForcedIdForConditionalIdPlaceholder(theId));
previouslyExisting = true;
} else if (!serverAssignedId) {
patient.setId(theId);
}
// Golden patients will be "GP#"
if (theId.length() >= 2 && theId.charAt(0) == 'G') {
@ -145,10 +155,20 @@ public class MdmLinkHelper {
}
MdmResourceUtil.setMdmManaged(patient);
DaoMethodOutcome outcome = myPatientDao.update(patient,
SystemRequestDetails.forAllPartitions());
SystemRequestDetails srd = SystemRequestDetails.forAllPartitions();
DaoMethodOutcome outcome;
if (serverAssignedId && !previouslyExisting) {
outcome = myPatientDao.create(patient, srd);
} else {
outcome = myPatientDao.update(patient, srd);
}
Patient outputPatient = (Patient) outcome.getResource();
theState.addPID(theId, (JpaPid) outcome.getPersistentId());
if (serverAssignedId) {
theState.addConditionalIdPlaceholderToForcedId(theId, outputPatient.getIdPart());
}
return outputPatient;
}
@ -209,9 +229,7 @@ public class MdmLinkHelper {
}
public List<MdmLink> getAllMdmLinks(Patient theGoldenPatient) {
return myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theGoldenPatient).stream()
.map( link -> (MdmLink) link)
.collect(Collectors.toList());
return new ArrayList<>(myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theGoldenPatient));
}
private boolean isResourcePartOfLink(

View File

@ -1,7 +1,10 @@
package ca.uhn.fhir.jpa.mdm.helper.testmodels;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import org.testcontainers.shaded.com.google.common.collect.HashMultimap;
import org.testcontainers.shaded.com.google.common.collect.Multimap;
@ -30,6 +33,7 @@ public class MDMState<T, P extends IResourcePersistentId> {
* Eg:
* PG1, MANUAL, MATCH, P1
* PG1, AUTO, POSSIBLE_MATCH, P2
* SERVER_ASSIGNED_GP, AUTO, POSSIBLE_MATCH, SERVER_ASSIGNED_P2
*/
private String myInputState;
@ -58,7 +62,9 @@ public class MDMState<T, P extends IResourcePersistentId> {
/**
* Map of forcedId -> resource persistent id for each resource created
*/
private final Map<String, P> myForcedIdToPID = new HashMap<>();
private final BiMap<String, P> myForcedIdToPID = HashBiMap.create();
private final Map<String, String> myConditionalIdPlaceholderToForcedId = new HashMap<>();
public void addPID(String theForcedId, P thePid) {
assert !myForcedIdToPID.containsKey(theForcedId);
@ -69,6 +75,23 @@ public class MDMState<T, P extends IResourcePersistentId> {
return myForcedIdToPID.get(theForcedId);
}
public String getForcedId(JpaPid thePID) {
String retVal = myForcedIdToPID.inverse().get(thePID);
if (myConditionalIdPlaceholderToForcedId.containsKey(retVal)) {
retVal = myConditionalIdPlaceholderToForcedId.get(retVal);
}
return retVal;
}
public void addConditionalIdPlaceholderToForcedId(String theConditionalIdPlaceholder, String theForcedId) {
assert !myConditionalIdPlaceholderToForcedId.containsKey(theConditionalIdPlaceholder);
myConditionalIdPlaceholderToForcedId.put(theConditionalIdPlaceholder, theForcedId);
}
public String getForcedIdForConditionalIdPlaceholder(String theConditionalIdPlaceholder) {
return myConditionalIdPlaceholderToForcedId.get(theConditionalIdPlaceholder);
}
public Multimap<T, MdmLink> getActualOutcomeLinks() {
if (myActualOutcomeLinks == null) {
myActualOutcomeLinks = HashMultimap.create();
@ -111,6 +134,18 @@ public class MDMState<T, P extends IResourcePersistentId> {
return myInputState;
}
/**
* Initial state for test.
* Comma separated lines with:
* Left param value, MdmLinkSourceEnum value, MdmMatchResultEnum value, Right param value
*
* Each input line represents a link state
*
* Eg:
* PG1, MANUAL, MATCH, P1
* PG1, AUTO, POSSIBLE_MATCH, P2
* SERVER_ASSIGNED_GP, AUTO, POSSIBLE_MATCH, SERVER_ASSIGNED_P2
*/
public MDMState<T, P> setInputState(String theInputState) {
myInputState = theInputState;
return this;

View File

@ -0,0 +1,409 @@
package ca.uhn.fhir.jpa.mdm.interceptor;
import ca.uhn.fhir.jpa.api.dao.PatientEverythingParameters;
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
import ca.uhn.fhir.jpa.mdm.helper.MdmHelperConfig;
import ca.uhn.fhir.jpa.mdm.helper.MdmHelperR4;
import ca.uhn.fhir.jpa.mdm.helper.MdmLinkHelper;
import ca.uhn.fhir.jpa.mdm.helper.testmodels.MDMLinkResults;
import ca.uhn.fhir.jpa.mdm.helper.testmodels.MDMState;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.mdm.interceptor.MdmReadVirtualizationInterceptor;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Encounter;
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.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.UUID;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.when;
@ContextConfiguration(classes = {MdmHelperConfig.class})
public class MdmReadVirtualizationInterceptorTest extends BaseMdmR4Test {
@RegisterExtension
@Autowired
public MdmHelperR4 myMdmHelper;
@Autowired
private MdmReadVirtualizationInterceptor<JpaPid> myInterceptor;
@Autowired
private MdmLinkHelper myLinkHelper;
private IIdType mySourcePatientA0Id;
private IIdType myGoldenResourcePatientAId;
private IIdType mySourcePatientA1Id;
private IIdType mySourcePatientA2Id;
private IIdType myObservationReferencingSourcePatientA0Id;
private IIdType myObservationReferencingSourcePatientA1Id;
private IIdType myObservationReferencingSourcePatientA2Id;
private IIdType myObservationReferencingGoldenPatientAId;
private IIdType mySourcePatientB0Id;
private IIdType myGoldenResourcePatientBId;
private IIdType myObservationReferencingSourcePatientB0Id;
@Override
@BeforeEach
public void before() throws Exception {
super.before();
}
@Override
@BeforeEach
public void after() throws IOException {
super.after();
myInterceptorRegistry.unregisterInterceptor(myInterceptor);
myInterceptorRegistry.unregisterAllAnonymousInterceptors();
}
@Test
public void testEverything() {
// Setup
createTestPatientsAndObservations(true);
registerVirtualizationInterceptor();
when(mySrd.getResourceName()).thenReturn("Patient");
when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE);
// Test
PatientEverythingParameters params = new PatientEverythingParameters();
IBundleProvider outcome = myPatientDao.patientInstanceEverything(null, mySrd, params, mySourcePatientA1Id);
// Verify
Map<String, IBaseResource> resources = toResourceIdValueMap(outcome);
List<String> ids = new ArrayList<>(resources.keySet());
assertThat(ids).asList().containsExactlyInAnyOrder(
mySourcePatientA1Id.getValue(),
myObservationReferencingGoldenPatientAId.getValue(),
myObservationReferencingSourcePatientA0Id.getValue(),
myObservationReferencingSourcePatientA1Id.getValue(),
myObservationReferencingSourcePatientA2Id.getValue()
);
assertEquals(mySourcePatientA1Id.getValue(), getObservation(resources, myObservationReferencingGoldenPatientAId).getSubject().getReference());
assertEquals(mySourcePatientA1Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA0Id).getSubject().getReference());
assertEquals(mySourcePatientA1Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA1Id).getSubject().getReference());
assertEquals(mySourcePatientA1Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA2Id).getSubject().getReference());
}
/**
* If we fetch an observation referencing a source patient, that reference should
* be remapped to the equivalent golden resource ID
*/
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testRead_ObservationReferencingSourcePatient(boolean theUseClientAssignedIds) {
// Setup
createTestPatientsAndObservations(theUseClientAssignedIds);
registerVirtualizationInterceptor();
when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.READ);
// Test
Observation obs = myObservationDao.read(myObservationReferencingSourcePatientA0Id, mySrd);
// Verify
assertEquals(mySourcePatientA0Id.getValue(), obs.getSubject().getReference());
}
/**
* If we fetch an observation referencing a golden resource, we should just
* leave it as is
*/
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testRead_ObservationReferencingGoldenPatient(boolean theUseClientAssignedIds) {
// Setup
createTestPatientsAndObservations(theUseClientAssignedIds);
registerVirtualizationInterceptor();
when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.READ);
// Test
Observation obs = myObservationDao.read(myObservationReferencingGoldenPatientAId, mySrd);
// Verify
assertEquals(myGoldenResourcePatientAId.getValue(), obs.getSubject().getReference());
}
/**
* If we search for all patients, only the golden resource ones should be returned
*/
@Test
public void testSearch_Patient_FetchAll() {
// Setup
createTestPatientsAndObservations(false);
registerVirtualizationInterceptor();
when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.SEARCH_TYPE);
// Test
IBundleProvider outcome = myPatientDao.search(SearchParameterMap.newSynchronous(), mySrd);
// Verify
List<String> ids = toUnqualifiedVersionlessIdValues(outcome);
assertThat(ids).asList().containsExactlyInAnyOrder(
mySourcePatientA0Id.getValue(),
mySourcePatientA1Id.getValue(),
mySourcePatientA2Id.getValue(),
mySourcePatientB0Id.getValue(),
myGoldenResourcePatientAId.getValue(),
myGoldenResourcePatientBId.getValue());
}
/**
* If we search for patients but only include source patients, these should be remapped to
* golden patients
*/
@Test
public void testSearch_Patient_FetchOnlySource() {
// Setup
createTestPatientsAndObservations(false);
registerVirtualizationInterceptor();
when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.SEARCH_TYPE);
// Test
SearchParameterMap params = SearchParameterMap.newSynchronous();
params.add(IAnyResource.SP_RES_ID, new TokenOrListParam()
.add(mySourcePatientA0Id.getValue())
.add(mySourcePatientB0Id.getValue()));
IBundleProvider outcome = myPatientDao.search(params, mySrd);
// Verify
List<String> ids = toUnqualifiedVersionlessIdValues(outcome);
assertThat(ids).asList().containsExactlyInAnyOrder(mySourcePatientA0Id.getValue(), mySourcePatientB0Id.getValue());
}
/**
* If we search for a patient by _id, and we _revinclude things pointing to the patient, we
* should also return things pointing to linked patients and update the references to
* point to that patient. The linked patients should not be included.
*/
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testSearch_Patient_FetchSourcePatient_AlsoRevIncludeDependentResources(boolean theUseClientAssginedId) {
// Setup
createTestPatientsAndObservations(theUseClientAssginedId);
registerVirtualizationInterceptor();
when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.SEARCH_TYPE);
// Test
SearchParameterMap params = SearchParameterMap.newSynchronous();
params.add(IAnyResource.SP_RES_ID, new TokenParam(mySourcePatientA2Id.getValue()));
params.addRevInclude(IBaseResource.INCLUDE_ALL);
IBundleProvider outcome = myPatientDao.search(params, mySrd);
// Verify
List<String> ids = toUnqualifiedVersionlessIdValues(outcome);
assertThat(ids).asList().containsExactlyInAnyOrder(
mySourcePatientA2Id.getValue(),
myObservationReferencingGoldenPatientAId.getValue(),
myObservationReferencingSourcePatientA0Id.getValue(),
myObservationReferencingSourcePatientA1Id.getValue(),
myObservationReferencingSourcePatientA2Id.getValue()
);
Map<String, IBaseResource> resources = toResourceIdValueMap(outcome);
assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingGoldenPatientAId).getSubject().getReference());
assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA0Id).getSubject().getReference());
assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA1Id).getSubject().getReference());
assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA2Id).getSubject().getReference());
}
@Test
public void testSearch_Observation_SpecificSourcePatient() {
// Setup
createTestPatientsAndObservations(true);
registerVirtualizationInterceptor();
when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.SEARCH_TYPE);
// Test
SearchParameterMap params = SearchParameterMap.newSynchronous();
params.add(Observation.SP_SUBJECT, new ReferenceParam(mySourcePatientA2Id.getValue()));
params.addInclude(Observation.INCLUDE_PATIENT);
IBundleProvider outcome = myObservationDao.search(params, mySrd);
// Verify
List<String> ids = toUnqualifiedVersionlessIdValues(outcome);
assertThat(ids).asList().containsExactlyInAnyOrder(
mySourcePatientA2Id.getValue(),
myObservationReferencingGoldenPatientAId.getValue(),
myObservationReferencingSourcePatientA0Id.getValue(),
myObservationReferencingSourcePatientA1Id.getValue(),
myObservationReferencingSourcePatientA2Id.getValue()
);
Map<String, IBaseResource> resources = toResourceIdValueMap(outcome);
assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingGoldenPatientAId).getSubject().getReference());
assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA0Id).getSubject().getReference());
assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA1Id).getSubject().getReference());
assertEquals(mySourcePatientA2Id.getValue(), getObservation(resources, myObservationReferencingSourcePatientA2Id).getSubject().getReference());
}
@Test
public void testSearch_Observation_NonRelativeReferencesAreLeftAlone() {
// Setup
createTestPatients(true);
registerVirtualizationInterceptor();
when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.SEARCH_TYPE);
IIdType obsId = createObservation(withSubject(mySourcePatientA0Id), withObservationCode("http://foo", "code0")).toUnqualifiedVersionless();
Observation obs = myObservationDao.read(obsId, mySrd);
Encounter encounter = new Encounter();
encounter.setId("1");
encounter.setStatus(Encounter.EncounterStatus.ARRIVED);
obs.getContained().add(encounter);
// Add 2 non-relative references. The interceptor should just ignore these
obs.setEncounter(new Reference("#1"));
obs.addBasedOn().setIdentifier(new Identifier().setValue("123"));
myObservationDao.update(obs, mySrd);
logAllResourceLinks();
// Test
SearchParameterMap params = SearchParameterMap.newSynchronous();
params.add(Observation.SP_SUBJECT, new ReferenceParam(mySourcePatientA2Id.getValue()));
params.addInclude(Observation.INCLUDE_PATIENT);
IBundleProvider outcome = myObservationDao.search(params, mySrd);
// Verify
List<String> ids = toUnqualifiedVersionlessIdValues(outcome);
assertThat(ids).asList().containsExactlyInAnyOrder(
mySourcePatientA2Id.getValue(),
obsId.getValue()
);
}
/**
* The CQL evaluator uses a shared RequestDetails across multiple different requests - Make sure
* we don't return the wrong cached results
*/
@Test
public void testSearch_RequestDetailsIsReused() {
// Setup
createTestPatientsAndObservations(true);
registerVirtualizationInterceptor();
when(mySrd.getRestOperationType()).thenReturn(RestOperationTypeEnum.SEARCH_TYPE);
// Test
SystemRequestDetails requestDetails = new SystemRequestDetails();
// Search for patients
requestDetails.setResourceName("Patient");
SearchParameterMap params = SearchParameterMap.newSynchronous();
params.add(IAnyResource.SP_RES_ID, new TokenParam(mySourcePatientA2Id.getValue()));
IBundleProvider outcome = myPatientDao.search(params, mySrd);
List<String> ids = toUnqualifiedVersionlessIdValues(outcome);
assertThat(ids).asList().containsExactlyInAnyOrder(
mySourcePatientA2Id.getValue()
);
// Search for Observations
requestDetails.setResourceName("Observation");
params = SearchParameterMap.newSynchronous();
params.add(Observation.SP_SUBJECT, new ReferenceParam(mySourcePatientA2Id.getValue()));
params.addInclude(Observation.INCLUDE_PATIENT);
outcome = myObservationDao.search(params, mySrd);
// Verify
ids = toUnqualifiedVersionlessIdValues(outcome);
assertThat(ids).asList().containsExactlyInAnyOrder(
mySourcePatientA2Id.getValue(),
myObservationReferencingGoldenPatientAId.getValue(),
myObservationReferencingSourcePatientA0Id.getValue(),
myObservationReferencingSourcePatientA1Id.getValue(),
myObservationReferencingSourcePatientA2Id.getValue()
);
}
private static Observation getObservation(Map<String, IBaseResource> resources, IIdType observationReferencingGoldenPatientAId) {
Observation retVal = (Observation) resources.get(observationReferencingGoldenPatientAId.getValue());
if (retVal == null) {
fail("Could not find '" + observationReferencingGoldenPatientAId.getValue() + "' - Valid IDs: " + new TreeSet<>(resources.keySet()));
}
return retVal;
}
private void registerVirtualizationInterceptor() {
myInterceptorRegistry.registerInterceptor(myInterceptor);
}
private void createTestPatientsAndObservations(boolean theUseClientAssignedIds) {
createTestPatients(theUseClientAssignedIds);
myObservationReferencingSourcePatientA0Id = createObservation(theUseClientAssignedIds, mySourcePatientA0Id, "code0");
myObservationReferencingSourcePatientA1Id = createObservation(theUseClientAssignedIds, mySourcePatientA1Id, "code1");
myObservationReferencingSourcePatientA2Id = createObservation(theUseClientAssignedIds, mySourcePatientA2Id, "code2");
myObservationReferencingGoldenPatientAId = createObservation(theUseClientAssignedIds, myGoldenResourcePatientAId, "code2");
myObservationReferencingSourcePatientB0Id = createObservation(theUseClientAssignedIds, mySourcePatientB0Id, "code0");
logAllResources();
}
private void createTestPatients(boolean theUseClientAssignedIds) {
String inputState;
if (theUseClientAssignedIds) {
inputState = """
GPA, AUTO, MATCH, PA0
GPA, AUTO, MATCH, PA1
GPA, AUTO, MATCH, PA2
GPB, AUTO, MATCH, PB0
""";
} else {
inputState = """
SERVER_ASSIGNED_GA, AUTO, MATCH, SERVER_ASSIGNED_PA0
SERVER_ASSIGNED_GA, AUTO, MATCH, SERVER_ASSIGNED_PA1
SERVER_ASSIGNED_GA, AUTO, MATCH, SERVER_ASSIGNED_PA2
SERVER_ASSIGNED_GB, AUTO, MATCH, SERVER_ASSIGNED_PB0
""";
}
MDMState<Patient, JpaPid> state = new MDMState<>();
state.setInputState(inputState);
MDMLinkResults outcome = myLinkHelper.setup(state);
mySourcePatientA0Id = toId(state, outcome.getResults().get(0).getSourcePersistenceId());
myGoldenResourcePatientAId = toId(state, outcome.getResults().get(0).getGoldenResourcePersistenceId());
mySourcePatientA1Id = toId(state, outcome.getResults().get(1).getSourcePersistenceId());
mySourcePatientA2Id = toId(state, outcome.getResults().get(2).getSourcePersistenceId());
mySourcePatientB0Id = toId(state, outcome.getResults().get(3).getSourcePersistenceId());
myGoldenResourcePatientBId = toId(state, outcome.getResults().get(3).getGoldenResourcePersistenceId());
assertEquals(4, logAllMdmLinks());
assertEquals(!theUseClientAssignedIds, mySourcePatientA0Id.isIdPartValidLong());
assertEquals(!theUseClientAssignedIds, myGoldenResourcePatientAId.isIdPartValidLong());
}
@Nonnull
private static IdType toId(MDMState<Patient, JpaPid> state, JpaPid persistentId) {
return new IdType("Patient/" + state.getForcedId(persistentId));
}
private IIdType createObservation(boolean theUseClientAssignedIds, IIdType patientId, String code) {
String resourceId = theUseClientAssignedIds ? UUID.randomUUID().toString() : null;
return createObservation(withIdOrNull(resourceId), withSubject(patientId), withObservationCode("http://foo", code)).toUnqualifiedVersionless();
}
}

View File

@ -26,7 +26,6 @@ import org.hl7.fhir.r4.model.Reference;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
@ -37,13 +36,10 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.slf4j.LoggerFactory.getLogger;
@ContextConfiguration(classes = {MdmHelperConfig.class})
public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
private static final Logger ourLog = getLogger(MdmSearchExpandingInterceptorIT.class);
@RegisterExtension
@Autowired
public MdmHelperR4 myMdmHelper;
@ -55,11 +51,10 @@ public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
* <p>
* Returns a list of stringified ids for the various resources.
* <p>
* Currently, order of returned resources is patientids first,
* observation ids next. But this can be refined as needed.
* Currently, order of returned resources is Patient IDs first,
* Observation IDs next. But this can be refined as needed.
*
* @param theResourceCount - number of patients to create
* @return
*/
private List<String> createAndLinkNewResources(int theResourceCount) throws InterruptedException {
boolean expansion = myStorageSettings.isAllowMdmExpansion();
@ -134,16 +129,16 @@ public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
//With MDM Expansion disabled, this should return 1 result.
myStorageSettings.setAllowMdmExpansion(false);
IBundleProvider search = myObservationDao.search(searchParameterMap);
IBundleProvider search = myObservationDao.search(searchParameterMap, mySrd);
assertEquals(1, search.size());
//Once MDM Expansion is allowed, this should now return 4 resourecs.
//Once MDM Expansion is allowed, this should now return 4 resources.
myStorageSettings.setAllowMdmExpansion(true);
search = myObservationDao.search(searchParameterMap);
search = myObservationDao.search(searchParameterMap, mySrd);
assertEquals(4, search.size());
List<MdmLink> all = myMdmLinkDao.findAll();
Long goldenPid = all.get(0).getGoldenResourcePid();
IIdType goldenId = myIdHelperService.translatePidIdToForcedId(myFhirContext, "Patient", JpaPid.fromId(goldenPid));
JpaPid goldenPid = all.get(0).getGoldenResourcePersistenceId();
IIdType goldenId = myIdHelperService.translatePidIdToForcedId(myFhirContext, "Patient", goldenPid);
//Verify that expansion by the golden resource id resolves down to everything its links have.
SearchParameterMap goldenSpMap = new SearchParameterMap();
@ -152,7 +147,7 @@ public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
goldenReferenceOrListParam.addOr(new ReferenceParam(goldenId).setMdmExpand(true));
goldenSpMap.add(Observation.SP_SUBJECT, goldenReferenceOrListParam);
search = myObservationDao.search(goldenSpMap);
search = myObservationDao.search(goldenSpMap, mySrd);
assertEquals(resourceCount, search.size());
}
@ -170,16 +165,16 @@ public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
//With MDM Expansion disabled, this should return 1 result.
myStorageSettings.setAllowMdmExpansion(false);
IBundleProvider search = myObservationDao.search(searchParameterMap);
IBundleProvider search = myObservationDao.search(searchParameterMap, mySrd);
assertEquals(1, search.size());
//Once MDM Expansion is allowed, this should now return 4 resourecs.
//Once MDM Expansion is allowed, this should now return 4 resources.
myStorageSettings.setAllowMdmExpansion(true);
search = myObservationDao.search(searchParameterMap);
search = myObservationDao.search(searchParameterMap, mySrd);
assertEquals(4, search.size());
List<MdmLink> all = myMdmLinkDao.findAll();
Long goldenPid = all.get(0).getGoldenResourcePid();
IIdType goldenId = myIdHelperService.translatePidIdToForcedId(myFhirContext, "Patient", JpaPid.fromId(goldenPid));
JpaPid goldenPid = all.get(0).getGoldenResourcePersistenceId();
IIdType goldenId = myIdHelperService.translatePidIdToForcedId(myFhirContext, "Patient", goldenPid);
//Verify that expansion by the golden resource id resolves down to everything its links have.
SearchParameterMap goldenSpMap = new SearchParameterMap();
@ -188,7 +183,7 @@ public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
goldenReferenceOrListParam.addOr(new ReferenceParam(goldenId).setMdmExpand(true));
goldenSpMap.add(Observation.SP_SUBJECT, goldenReferenceOrListParam);
search = myObservationDao.search(goldenSpMap);
search = myObservationDao.search(goldenSpMap, mySrd);
assertEquals(resourceCount, search.size());
}
@ -208,7 +203,7 @@ public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
orListParam.add(patientIdParam);
map.add("_id", orListParam);
IBundleProvider outcome = myPatientDao.search(map);
IBundleProvider outcome = myPatientDao.search(map, mySrd);
assertNotNull(outcome);
// we know 4 cause that's how many patients are created
@ -267,7 +262,7 @@ public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
//Even though the user has NO mdm links, that should not cause a request failure.
SearchParameterMap map = new SearchParameterMap();
map.add(Observation.SP_SUBJECT, new ReferenceParam("Patient/" + id).setMdmExpand(true));
IBundleProvider search = myObservationDao.search(map);
IBundleProvider search = myObservationDao.search(map, mySrd);
assertEquals(1, search.size());
}
@ -275,7 +270,7 @@ public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
Observation observation = new Observation();
observation.setSubject(new Reference("Patient/" + thePatientId));
observation.setCode(new CodeableConcept().setText("Made for Patient/" + thePatientId));
DaoMethodOutcome daoMethodOutcome = myObservationDao.create(observation);
DaoMethodOutcome daoMethodOutcome = myObservationDao.create(observation, mySrd);
return (Observation) daoMethodOutcome.getResource();
}
}

View File

@ -166,7 +166,7 @@ public class MdmOperationPointcutsIT extends BaseProviderR4Test {
Patient gp1 = state.getParameter("GP1");
Patient gp2 = state.getParameter("GP2");
Object intereptor = new Object() {
Object interceptor = new Object() {
@Hook(Pointcut.MDM_POST_MERGE_GOLDEN_RESOURCES)
void onUpdate(RequestDetails theDetails, MdmMergeEvent theEvent) {
called.getAndSet(true);
@ -175,8 +175,8 @@ public class MdmOperationPointcutsIT extends BaseProviderR4Test {
assertTrue(theEvent.getFromResource().isGoldenResource() && theEvent.getToResource().isGoldenResource());
}
};
myInterceptors.add(intereptor);
myInterceptorService.registerInterceptor(intereptor);
myInterceptors.add(interceptor);
myInterceptorService.registerInterceptor(interceptor);
// test
myMdmProvider.mergeGoldenResources(
@ -206,7 +206,7 @@ public class MdmOperationPointcutsIT extends BaseProviderR4Test {
MdmMatchResultEnum toSave = MdmMatchResultEnum.MATCH;
AtomicBoolean called = new AtomicBoolean(false);
Object intereptor = new Object() {
Object interceptor = new Object() {
@Hook(Pointcut.MDM_POST_UPDATE_LINK)
void onUpdate(RequestDetails theDetails, MdmLinkEvent theEvent) {
called.getAndSet(true);
@ -217,8 +217,8 @@ public class MdmOperationPointcutsIT extends BaseProviderR4Test {
assertEquals("Patient/" + gp1.getIdPart(), link.getGoldenResourceId());
}
};
myInterceptors.add(intereptor);
myInterceptorService.registerInterceptor(intereptor);
myInterceptors.add(interceptor);
myInterceptorService.registerInterceptor(interceptor);
// test
myMdmProvider.updateLink(
@ -240,7 +240,7 @@ public class MdmOperationPointcutsIT extends BaseProviderR4Test {
Patient golden = createGoldenPatient();
MdmMatchResultEnum match = MdmMatchResultEnum.MATCH;
Object intereptor = new Object() {
Object interceptor = new Object() {
@Hook(Pointcut.MDM_POST_CREATE_LINK)
void onCreate(RequestDetails theDetails, MdmLinkEvent theEvent) {
called.getAndSet(true);
@ -251,8 +251,8 @@ public class MdmOperationPointcutsIT extends BaseProviderR4Test {
assertEquals("Patient/" + golden.getIdPart(), link.getGoldenResourceId());
}
};
myInterceptors.add(intereptor);
myInterceptorService.registerInterceptor(intereptor);
myInterceptors.add(interceptor);
myInterceptorService.registerInterceptor(interceptor);
// test
myMdmProvider.createLink(

View File

@ -208,7 +208,7 @@ public class MdmMatchLinkSvcMultipleEidModeTest extends BaseMdmR4Test {
addExternalEID(patient2, "eid-11");
addExternalEID(patient2, "eid-22");
patient2 = updatePatientAndUpdateLinks(patient2);
logAllLinks();
logAllMdmLinks();
assertLinksMatchResult(MATCH, POSSIBLE_MATCH, MATCH, POSSIBLE_MATCH, POSSIBLE_DUPLICATE);
assertLinksCreatedNewResource(true, true, false, false, false);
assertLinksMatchedByEid(false, true, true, true, true);

View File

@ -495,7 +495,7 @@ public class MdmMatchLinkSvcTest {
Optional<? extends IMdmLink> matchedLinkForTargetPid = runInTransaction(() -> myMdmLinkDaoSvc.getMatchedLinkForSourcePid(myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), incomingJanePatient)));
assertThat(matchedLinkForTargetPid.isPresent()).isEqualTo(false);
logAllLinks();
logAllMdmLinks();
assertLinksMatchResult(MATCH, MATCH, POSSIBLE_MATCH, POSSIBLE_MATCH, POSSIBLE_DUPLICATE);
assertLinksCreatedNewResource(true, true, false, false, false);
assertLinksMatchedByEid(false, false, false, false, false);

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -539,7 +539,7 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil
@Autowired
protected ValidationSettings myValidationSettings;
@Autowired
protected IMdmLinkJpaRepository myMdmLinkDao;
protected IMdmLinkJpaRepository myMdmLinkRepository;
@Autowired
protected IMdmLinkJpaRepository myMdmLinkHistoryDao;
@Autowired

View File

@ -56,12 +56,14 @@ import ca.uhn.fhir.jpa.dao.data.ITermConceptDesignationDao;
import ca.uhn.fhir.jpa.dao.data.ITermConceptPropertyDao;
import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDao;
import ca.uhn.fhir.jpa.dao.data.ITermValueSetDao;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.entity.TermConcept;
import ca.uhn.fhir.jpa.entity.TermConceptDesignation;
import ca.uhn.fhir.jpa.entity.TermConceptProperty;
import ca.uhn.fhir.jpa.entity.TermValueSet;
import ca.uhn.fhir.jpa.entity.TermValueSetConcept;
import ca.uhn.fhir.jpa.entity.TermValueSetConceptDesignation;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique;
@ -85,6 +87,7 @@ import ca.uhn.fhir.jpa.model.config.SubscriptionSettings;
import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.mdm.dao.IMdmLinkDao;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@ -143,6 +146,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
@ -195,6 +199,8 @@ public abstract class BaseJpaTest extends BaseTest {
protected ServletRequestDetails mySrd;
protected InterceptorService mySrdInterceptorService;
@Autowired
protected IMdmLinkDao<JpaPid, MdmLink> myMdmLinkDao;
@Autowired
protected FhirContext myFhirContext;
@Autowired
protected JpaStorageSettings myStorageSettings;
@ -487,6 +493,22 @@ public abstract class BaseJpaTest extends BaseTest {
});
}
protected int countAllMdmLinks() {
return runInTransaction(()-> myMdmLinkDao.findAll().size());
}
protected int logAllMdmLinks() {
return runInTransaction(()->{
List<MdmLink> links = myMdmLinkDao.findAll();
if (links.isEmpty()) {
ourLog.info("MDM Links: NONE");
} else {
ourLog.info("MDM Links:\n * {}", links.stream().map(t -> t.toString()).collect(joining("\n * ")));
}
return links.size();
});
}
protected void logAllResourceLinks() {
runInTransaction(() -> {
ourLog.info("Resource Links:\n * {}", myResourceLinkDao.findAll().stream().map(ResourceLink::toString).collect(Collectors.joining("\n * ")));
@ -698,6 +720,19 @@ public abstract class BaseJpaTest extends BaseTest {
return toUnqualifiedVersionlessIdValues(theFound, fromIndex, toIndex, true);
}
/**
* Keys will be unqualified versionless IDs (Patient/ABC) and values will be the resources
* themselves.
*/
protected Map<String, IBaseResource> toResourceIdValueMap(IBundleProvider theFound) {
Map<String, IBaseResource> retVal = new HashMap<>();
List<IBaseResource> resources = theFound.getAllResources();
for (IBaseResource next : resources) {
retVal.put(next.getIdElement().toUnqualifiedVersionless().getValue(), next);
}
return retVal;
}
protected List<String> toUnqualifiedVersionlessIdValues(IBundleProvider theFound, int theFromIndex, Integer theToIndex, boolean theFirstCall) {
if (theToIndex == null) {
theToIndex = 99999;

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -20,7 +20,6 @@
package ca.uhn.fhir.mdm.api;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -41,5 +40,5 @@ public interface IMdmLinkExpandSvc {
Set<String> expandMdmByGoldenResourcePid(
RequestPartitionId theRequestPartitionId, IResourcePersistentId<?> theGoldenResourcePid);
Set<String> expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IdDt theId);
Set<String> expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IIdType theId);
}

View File

@ -53,4 +53,17 @@ public class MdmConstants {
"This resource was found to be a duplicate and has been redirected.";
public static final String UNKNOWN_MDM_TYPES = "Unknown Resource Types";
/**
* Interceptor order constant for {@link ca.uhn.fhir.mdm.interceptor.MdmReadVirtualizationInterceptor}, which
* should fire before {@link ca.uhn.fhir.mdm.interceptor.MdmSearchExpandingInterceptor} since it is a
* superset of the same functionality and only one should run if they are both registered for whatever
* reason.
*/
public static final int ORDER_PRESEARCH_REGISTERED_MDM_READ_VIRTUALIZATION_INTERCEPTOR = 0;
/**
* @see #ORDER_PRESEARCH_REGISTERED_MDM_READ_VIRTUALIZATION_INTERCEPTOR
*/
public static final int ORDER_PRESEARCH_REGISTERED_MDM_SEARCH_EXPANDING_INTERCEPTOR = 1;
}

View File

@ -36,6 +36,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.history.Revisions;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@ -110,4 +111,12 @@ public interface IMdmLinkDao<P extends IResourcePersistentId, M extends IMdmLink
default List<MdmLinkWithRevision<M>> getHistoryForIds(MdmHistorySearchParameters theMdmHistorySearchParameters) {
throw new UnsupportedOperationException(Msg.code(2299) + "not yet implemented");
}
/**
* Given a collection of PIDs, resolves the associated golden resource IDs. If any of the PIDs
* are golden resources, the associated non-golden resources are also fetched.
*/
default Collection<MdmPidTuple<P>> resolveGoldenResources(List<P> theSourcePids) {
throw new UnsupportedOperationException(Msg.code(2568) + "not yet implemented");
}
}

View File

@ -0,0 +1,213 @@
/*-
* #%L
* HAPI FHIR - Master Data Management
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.interceptor;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.Hook;
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.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.mdm.api.MdmConstants;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.svc.MdmSearchExpansionResults;
import ca.uhn.fhir.mdm.svc.MdmSearchExpansionSvc;
import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.ResourceReferenceInfo;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* <b>This class is experimental and subject to change. Use with caution.</b>
* <p>
* This interceptor provides an "MDM Virtualized" endpoint, meaning that
* searches are expanded to include MDM-linked resources (including any
* linked golden resource, and also including any other resources linked
* to that golden resource). Searches for non-MDM resources which have
* a reference to an MDM resource will have their reference parameter
* expanded to include the golden and linked resources.
* </p>
* <p>
* In addition, responses are cleaned up so that only the golden resource
* is included in responses, and references to non-golden resources
* are rewritten.
* </p>
* <p>
* This interceptor does not modify data that is being stored/written
* in any way, it only modifies data that is being returned by the
* server.
* </p>
*
* @since 8.0.0
*/
public class MdmReadVirtualizationInterceptor<P extends IResourcePersistentId<?>> {
private static final Logger ourMdmTroubleshootingLog = Logs.getMdmTroubleshootingLog();
private static final String CURRENTLY_PROCESSING_FLAG =
MdmReadVirtualizationInterceptor.class.getName() + "_CURRENTLY_PROCESSING";
private static final MdmSearchExpansionSvc.IParamTester PARAM_TESTER_NO_RES_ID =
(paramName, param) -> !IAnyResource.SP_RES_ID.equals(paramName);
private static final MdmSearchExpansionSvc.IParamTester PARAM_TESTER_ALL = (paramName, param) -> true;
@Autowired
private FhirContext myFhirContext;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private MdmSearchExpansionSvc myMdmSearchExpansionSvc;
@Hook(
value = Pointcut.STORAGE_PRESEARCH_REGISTERED,
order = MdmConstants.ORDER_PRESEARCH_REGISTERED_MDM_READ_VIRTUALIZATION_INTERCEPTOR)
public void hook(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) {
ourMdmTroubleshootingLog
.atTrace()
.setMessage("MDM virtualization original search: {}{}")
.addArgument(theRequestDetails.getResourceName())
.addArgument(() -> theSearchParameterMap.toNormalizedQueryString(myFhirContext))
.log();
if (theSearchParameterMap.hasIncludes() || theSearchParameterMap.hasRevIncludes()) {
myMdmSearchExpansionSvc.expandSearchAndStoreInRequestDetails(
theRequestDetails, theSearchParameterMap, PARAM_TESTER_ALL);
} else {
// If we don't have any includes, it's not worth auto-expanding the _id parameter since we'll only end
// up filtering out the extra resources afterward
myMdmSearchExpansionSvc.expandSearchAndStoreInRequestDetails(
theRequestDetails, theSearchParameterMap, PARAM_TESTER_NO_RES_ID);
}
ourMdmTroubleshootingLog
.atDebug()
.setMessage("MDM virtualization remapped search: {}{}")
.addArgument(theRequestDetails.getResourceName())
.addArgument(() -> theSearchParameterMap.toNormalizedQueryString(myFhirContext))
.log();
}
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
@Hook(Pointcut.STORAGE_PRESHOW_RESOURCES)
public void preShowResources(RequestDetails theRequestDetails, IPreResourceShowDetails theDetails) {
MdmSearchExpansionResults expansionResults = MdmSearchExpansionSvc.getCachedExpansionResults(theRequestDetails);
if (expansionResults == null) {
// This means the PRESEARCH hook didn't save anything, which probably means
// no RequestDetails is available
return;
}
if (theRequestDetails.getUserData().get(CURRENTLY_PROCESSING_FLAG) != null) {
// Avoid recursive calls
return;
}
/*
* If a resource being returned is a resource that was mdm-expanded,
* we'll replace that resource with the originally requested resource,
* making sure to avoid adding duplicates to the results.
*/
Set<IIdType> resourcesInBundle = new HashSet<>();
for (int resourceIdx = 0; resourceIdx < theDetails.size(); resourceIdx++) {
IBaseResource resource = theDetails.getResource(resourceIdx);
IIdType id = resource.getIdElement().toUnqualifiedVersionless();
Optional<IIdType> originalIdOpt = expansionResults.getOriginalIdForExpandedId(id);
if (originalIdOpt.isPresent()) {
IIdType originalId = originalIdOpt.get();
if (resourcesInBundle.add(originalId)) {
IBaseResource originalResource = fetchResourceFromRepository(theRequestDetails, originalId);
theDetails.setResource(resourceIdx, originalResource);
} else {
theDetails.setResource(resourceIdx, null);
}
} else {
if (!resourcesInBundle.add(id)) {
theDetails.setResource(resourceIdx, null);
}
}
}
FhirTerser terser = myFhirContext.newTerser();
for (int resourceIdx = 0; resourceIdx < theDetails.size(); resourceIdx++) {
IBaseResource resource = theDetails.getResource(resourceIdx);
if (resource != null) {
// Extract all the references in the resources we're returning
// in case we need to remap them to golden equivalents
List<ResourceReferenceInfo> referenceInfos = terser.getAllResourceReferences(resource);
for (ResourceReferenceInfo referenceInfo : referenceInfos) {
IIdType referenceId = referenceInfo
.getResourceReference()
.getReferenceElement()
.toUnqualifiedVersionless();
if (referenceId.hasResourceType()
&& referenceId.hasIdPart()
&& !referenceId.isLocal()
&& !referenceId.isUuid()) {
Optional<IIdType> nonExpandedId = expansionResults.getOriginalIdForExpandedId(referenceId);
if (nonExpandedId != null && nonExpandedId.isPresent()) {
ourMdmTroubleshootingLog.debug(
"MDM virtualization is replacing reference at {} value {} with {}",
referenceInfo.getName(),
referenceInfo.getResourceReference().getReferenceElement(),
nonExpandedId.get().getValue());
referenceInfo
.getResourceReference()
.setReference(nonExpandedId.get().getValue());
}
}
}
}
}
ourMdmTroubleshootingLog
.atTrace()
.setMessage("Returning resources: {}")
.addArgument(() -> theDetails.getAllResources().stream()
.map(t -> t.getIdElement().toUnqualifiedVersionless().getValue())
.sorted()
.collect(Collectors.toList()))
.log();
}
private IBaseResource fetchResourceFromRepository(RequestDetails theRequestDetails, IIdType originalId) {
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(originalId.getResourceType());
theRequestDetails.getUserData().put(CURRENTLY_PROCESSING_FLAG, Boolean.TRUE);
IBaseResource originalResource;
try {
originalResource = dao.read(originalId, theRequestDetails);
} finally {
theRequestDetails.getUserData().remove(CURRENTLY_PROCESSING_FLAG);
}
return originalResource;
}
}

View File

@ -22,174 +22,46 @@ package ca.uhn.fhir.mdm.interceptor;
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.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.mdm.api.IMdmLinkExpandSvc;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.mdm.api.MdmConstants;
import ca.uhn.fhir.mdm.svc.MdmSearchExpansionSvc;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.TokenParam;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* This interceptor replaces the auto-generated CapabilityStatement that is generated
* by the HAPI FHIR Server with a static hard-coded resource.
*/
@Interceptor
public class MdmSearchExpandingInterceptor {
// A simple interface to turn ids into some form of IQueryParameterTypes
private interface Creator<T extends IQueryParameterType> {
T create(String id);
}
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
@Autowired
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
@Autowired
private IMdmLinkExpandSvc myMdmLinkExpandSvc;
private static final MdmSearchExpansionSvc.IParamTester PARAM_TESTER = (paramName, param) -> {
boolean retVal = false;
if (param instanceof ReferenceParam) {
retVal = ((ReferenceParam) param).isMdmExpand();
} else if (param instanceof TokenParam) {
retVal = ((TokenParam) param).isMdmExpand();
}
return retVal;
};
@Autowired
private JpaStorageSettings myStorageSettings;
@Hook(Pointcut.STORAGE_PRESEARCH_REGISTERED)
@Autowired
private MdmSearchExpansionSvc myMdmSearchExpansionSvc;
@Hook(
value = Pointcut.STORAGE_PRESEARCH_REGISTERED,
order = MdmConstants.ORDER_PRESEARCH_REGISTERED_MDM_SEARCH_EXPANDING_INTERCEPTOR)
public void hook(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) {
if (myStorageSettings.isAllowMdmExpansion()) {
final RequestDetails requestDetailsToUse =
theRequestDetails == null ? new SystemRequestDetails() : theRequestDetails;
final RequestPartitionId requestPartitionId =
myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType(
requestDetailsToUse, requestDetailsToUse.getResourceName(), theSearchParameterMap);
for (Map.Entry<String, List<List<IQueryParameterType>>> set : theSearchParameterMap.entrySet()) {
String paramName = set.getKey();
List<List<IQueryParameterType>> andList = set.getValue();
for (List<IQueryParameterType> orList : andList) {
// here we will know if it's an _id param or not
// from theSearchParameterMap.keySet()
expandAnyReferenceParameters(requestPartitionId, paramName, orList);
}
}
myMdmSearchExpansionSvc.expandSearchAndStoreInRequestDetails(
theRequestDetails, theSearchParameterMap, PARAM_TESTER);
}
}
/**
* If a Parameter is a reference parameter, and it has been set to expand MDM, perform the expansion.
*/
private void expandAnyReferenceParameters(
RequestPartitionId theRequestPartitionId, String theParamName, List<IQueryParameterType> orList) {
List<IQueryParameterType> toRemove = new ArrayList<>();
List<IQueryParameterType> toAdd = new ArrayList<>();
for (IQueryParameterType iQueryParameterType : orList) {
if (iQueryParameterType instanceof ReferenceParam) {
ReferenceParam refParam = (ReferenceParam) iQueryParameterType;
if (refParam.isMdmExpand()) {
ourLog.debug("Found a reference parameter to expand: {}", refParam);
// First, attempt to expand as a source resource.
Set<String> expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(
theRequestPartitionId, new IdDt(refParam.getValue()));
// If we failed, attempt to expand as a golden resource
if (expandedResourceIds.isEmpty()) {
expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId(
theRequestPartitionId, new IdDt(refParam.getValue()));
}
// Rebuild the search param list.
if (!expandedResourceIds.isEmpty()) {
ourLog.debug("Parameter has been expanded to: {}", String.join(", ", expandedResourceIds));
toRemove.add(refParam);
expandedResourceIds.stream()
.map(resourceId -> addResourceTypeIfNecessary(refParam.getResourceType(), resourceId))
.map(ReferenceParam::new)
.forEach(toAdd::add);
}
}
} else if (theParamName.equalsIgnoreCase("_id")) {
expandIdParameter(theRequestPartitionId, iQueryParameterType, toAdd, toRemove);
}
}
orList.removeAll(toRemove);
orList.addAll(toAdd);
}
private String addResourceTypeIfNecessary(String theResourceType, String theResourceId) {
if (theResourceId.contains("/")) {
return theResourceId;
} else {
return theResourceType + "/" + theResourceId;
}
}
/**
* Expands out the provided _id parameter into all the various
* ids of linked resources.
*
* @param theRequestPartitionId
* @param theIdParameter
* @param theAddList
* @param theRemoveList
*/
private void expandIdParameter(
RequestPartitionId theRequestPartitionId,
IQueryParameterType theIdParameter,
List<IQueryParameterType> theAddList,
List<IQueryParameterType> theRemoveList) {
// id parameters can either be StringParam (for $everything operation)
// or TokenParam (for searches)
// either case, we want to expand it out and grab all related resources
IIdType id;
Creator<? extends IQueryParameterType> creator;
boolean mdmExpand = false;
if (theIdParameter instanceof TokenParam) {
TokenParam param = (TokenParam) theIdParameter;
mdmExpand = param.isMdmExpand();
id = new IdDt(param.getValue());
creator = TokenParam::new;
} else {
creator = null;
id = null;
}
if (id == null) {
// in case the _id paramter type is different from the above
ourLog.warn(
"_id parameter of incorrect type. Expected StringParam or TokenParam, but got {}. No expansion will be done!",
theIdParameter.getClass().getSimpleName());
} else if (mdmExpand) {
ourLog.debug("_id parameter must be expanded out from: {}", id.getValue());
Set<String> expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(theRequestPartitionId, id);
if (expandedResourceIds.isEmpty()) {
expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId(theRequestPartitionId, (IdDt) id);
}
// Rebuild
if (!expandedResourceIds.isEmpty()) {
ourLog.debug("_id parameter has been expanded to: {}", String.join(", ", expandedResourceIds));
// remove the original
theRemoveList.add(theIdParameter);
// add in all the linked values
expandedResourceIds.stream().map(creator::create).forEach(theAddList::add);
}
}
// else - no expansion required
}
}

View File

@ -26,7 +26,6 @@ import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.dao.IMdmLinkDao;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.model.MdmPidTuple;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -137,7 +136,7 @@ public class MdmLinkExpandSvc implements IMdmLinkExpandSvc {
}
@Override
public Set<String> expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IdDt theId) {
public Set<String> expandMdmByGoldenResourceId(RequestPartitionId theRequestPartitionId, IIdType theId) {
ourLog.debug("About to expand golden resource with golden resource id {}", theId);
IResourcePersistentId<?> pidOrThrowException =
myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theId);

View File

@ -0,0 +1,68 @@
/*-
* #%L
* HAPI FHIR - Master Data Management
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.svc;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* Result object for {@link MdmSearchExpansionSvc}
*
* @since 8.0.0
*/
public class MdmSearchExpansionResults {
private final Set<IIdType> myOriginalIdToExpandedId = new HashSet<>();
private final Map<IIdType, IIdType> myExpandedIdToOriginalId = new HashMap<>();
void addExpandedId(IIdType theOriginalId, IIdType theExpandedId) {
assert isRemapCandidate(theOriginalId) : theOriginalId.getValue();
myOriginalIdToExpandedId.add(theOriginalId);
myExpandedIdToOriginalId.put(theExpandedId, theOriginalId);
}
public Optional<IIdType> getOriginalIdForExpandedId(IIdType theId) {
assert isRemapCandidate(theId) : theId.getValue();
// If we have this ID in the OriginalId map, it was explicitly
// searched for, so even if it's also an expanded ID we don't
// want to consider it as one
if (myOriginalIdToExpandedId.contains(theId)) {
return Optional.empty();
}
IIdType originalId = myExpandedIdToOriginalId.get(theId);
return Optional.ofNullable(originalId);
}
public static boolean isRemapCandidate(IIdType theId) {
return theId != null
&& !theId.isLocal()
&& !theId.isUuid()
&& theId.hasResourceType()
&& theId.hasIdPart()
&& theId.getValue().equals(theId.toUnqualifiedVersionless().getValue());
}
}

View File

@ -0,0 +1,275 @@
/*-
* #%L
* HAPI FHIR - Master Data Management
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.svc;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.mdm.api.IMdmLinkExpandSvc;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.BaseParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.param.TokenParam;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class MdmSearchExpansionSvc {
private static final String EXPANSION_RESULTS = MdmSearchExpansionSvc.class.getName() + "_EXPANSION_RESULTS";
private static final String RESOURCE_NAME = MdmSearchExpansionSvc.class.getName() + "_RESOURCE_NAME";
private static final String QUERY_STRING = MdmSearchExpansionSvc.class.getName() + "_QUERY_STRING";
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
@Autowired
private FhirContext myFhirContext;
@Autowired
private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
@Autowired
private IMdmLinkExpandSvc myMdmLinkExpandSvc;
/**
* This method looks through all the reference parameters within a {@link SearchParameterMap}
* and performs MDM expansion. This means looking for any subject/patient parameters, and
* expanding them to include any linked and golden resource patients. So, for example, a
* search for <code>Encounter?subject=Patient/1</code> might be modified to be a search
* for <code>Encounter?subject=Patient/1,Patient/999</code> if 999 is linked to 1 by MDM.
* <p>
* This is an internal MDM service and its API is subject to change. Use with caution!
* </p>
*
* @param theRequestDetails The incoming request details
* @param theSearchParameterMap The parameter map to modify
* @param theParamTester Determines which parameters should be expanded
* @return Returns the results of the expansion, which are also stored in the {@link RequestDetails} userdata map, so this service will only be invoked a maximum of once per request.
* @since 8.0.0
*/
public MdmSearchExpansionResults expandSearchAndStoreInRequestDetails(
@Nullable RequestDetails theRequestDetails,
@Nonnull SearchParameterMap theSearchParameterMap,
IParamTester theParamTester) {
if (theRequestDetails == null) {
return null;
}
// Try to detect if the RequestDetails is being reused across multiple different queries, which
// can happen during CQL measure evaluation
String resourceName = theRequestDetails.getResourceName();
String queryString = theSearchParameterMap.toNormalizedQueryString(myFhirContext);
if (!Objects.equals(resourceName, theRequestDetails.getUserData().get(RESOURCE_NAME))
|| !Objects.equals(queryString, theRequestDetails.getUserData().get(QUERY_STRING))) {
theRequestDetails.getUserData().remove(EXPANSION_RESULTS);
}
theRequestDetails.getUserData().put(RESOURCE_NAME, resourceName);
theRequestDetails.getUserData().put(QUERY_STRING, queryString);
MdmSearchExpansionResults expansionResults = getCachedExpansionResults(theRequestDetails);
if (expansionResults != null) {
return expansionResults;
}
expansionResults = new MdmSearchExpansionResults();
final RequestPartitionId requestPartitionId =
myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType(
theRequestDetails, theRequestDetails.getResourceName(), theSearchParameterMap);
for (Map.Entry<String, List<List<IQueryParameterType>>> set : theSearchParameterMap.entrySet()) {
String paramName = set.getKey();
List<List<IQueryParameterType>> andList = set.getValue();
for (List<IQueryParameterType> orList : andList) {
// here we will know if it's an _id param or not
// from theSearchParameterMap.keySet()
expandAnyReferenceParameters(
requestPartitionId,
theRequestDetails.getResourceName(),
paramName,
orList,
theParamTester,
expansionResults);
}
}
theRequestDetails.getUserData().put(EXPANSION_RESULTS, expansionResults);
return expansionResults;
}
private void expandAnyReferenceParameters(
RequestPartitionId theRequestPartitionId,
String theResourceName,
String theParamName,
List<IQueryParameterType> orList,
IParamTester theParamTester,
MdmSearchExpansionResults theResultsToPopulate) {
List<IQueryParameterType> toRemove = new ArrayList<>();
List<IQueryParameterType> toAdd = new ArrayList<>();
for (IQueryParameterType iQueryParameterType : orList) {
if (iQueryParameterType instanceof ReferenceParam) {
ReferenceParam refParam = (ReferenceParam) iQueryParameterType;
if (theParamTester.shouldExpand(theParamName, refParam)) {
ourLog.debug("Found a reference parameter to expand: {}", refParam);
// First, attempt to expand as a source resource.
IIdType sourceId = newId(refParam.getValue());
Set<String> expandedResourceIds =
myMdmLinkExpandSvc.expandMdmBySourceResourceId(theRequestPartitionId, sourceId);
// If we failed, attempt to expand as a golden resource
if (expandedResourceIds.isEmpty()) {
expandedResourceIds =
myMdmLinkExpandSvc.expandMdmByGoldenResourceId(theRequestPartitionId, sourceId);
}
// Rebuild the search param list.
if (!expandedResourceIds.isEmpty()) {
ourLog.debug("Parameter has been expanded to: {}", String.join(", ", expandedResourceIds));
toRemove.add(refParam);
for (String resourceId : expandedResourceIds) {
IIdType nextReference =
newId(addResourceTypeIfNecessary(refParam.getResourceType(), resourceId));
toAdd.add(new ReferenceParam(nextReference));
theResultsToPopulate.addExpandedId(sourceId, nextReference);
}
}
}
} else if (theParamName.equalsIgnoreCase(IAnyResource.SP_RES_ID)) {
expandIdParameter(
theRequestPartitionId,
iQueryParameterType,
toAdd,
toRemove,
theParamTester,
theResourceName,
theResultsToPopulate);
}
}
orList.removeAll(toRemove);
orList.addAll(toAdd);
}
private IIdType newId(String value) {
return myFhirContext.getVersion().newIdType(value);
}
private String addResourceTypeIfNecessary(String theResourceType, String theResourceId) {
if (theResourceId.contains("/")) {
return theResourceId;
} else {
return theResourceType + "/" + theResourceId;
}
}
/**
* Expands out the provided _id parameter into all the various
* ids of linked resources.
*/
private void expandIdParameter(
RequestPartitionId theRequestPartitionId,
IQueryParameterType theIdParameter,
List<IQueryParameterType> theAddList,
List<IQueryParameterType> theRemoveList,
IParamTester theParamTester,
String theResourceName,
MdmSearchExpansionResults theResultsToPopulate) {
// id parameters can either be StringParam (for $everything operation)
// or TokenParam (for searches)
// either case, we want to expand it out and grab all related resources
IIdType id;
Creator<? extends IQueryParameterType> creator;
boolean mdmExpand = false;
if (theIdParameter instanceof TokenParam) {
TokenParam param = (TokenParam) theIdParameter;
mdmExpand = theParamTester.shouldExpand(IAnyResource.SP_RES_ID, param);
String value = param.getValue();
value = addResourceTypeIfNecessary(theResourceName, value);
id = newId(value);
creator = TokenParam::new;
} else {
creator = null;
id = null;
}
if (id == null) {
// in case the _id parameter type is different from the above
ourLog.warn(
"_id parameter of incorrect type. Expected StringParam or TokenParam, but got {}. No expansion will be done!",
theIdParameter.getClass().getSimpleName());
} else if (mdmExpand) {
ourLog.debug("_id parameter must be expanded out from: {}", id.getValue());
Set<String> expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(theRequestPartitionId, id);
if (expandedResourceIds.isEmpty()) {
expandedResourceIds = myMdmLinkExpandSvc.expandMdmByGoldenResourceId(theRequestPartitionId, id);
}
// Rebuild
if (!expandedResourceIds.isEmpty()) {
ourLog.debug("_id parameter has been expanded to: {}", expandedResourceIds);
// remove the original
theRemoveList.add(theIdParameter);
// add in all the linked values
expandedResourceIds.stream().map(creator::create).forEach(theAddList::add);
for (String expandedId : expandedResourceIds) {
theResultsToPopulate.addExpandedId(
id, newId(addResourceTypeIfNecessary(theResourceName, expandedId)));
}
}
}
// else - no expansion required
}
// A simple interface to turn ids into some form of IQueryParameterTypes
private interface Creator<T extends IQueryParameterType> {
T create(String id);
}
@FunctionalInterface
public interface IParamTester {
boolean shouldExpand(String theParamName, BaseParam theParam);
}
@Nullable
public static MdmSearchExpansionResults getCachedExpansionResults(@Nonnull RequestDetails theRequestDetails) {
MdmSearchExpansionResults expansionResults =
(MdmSearchExpansionResults) theRequestDetails.getUserData().get(EXPANSION_RESULTS);
return expansionResults;
}
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -1,6 +1,6 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%

View File

@ -1,6 +1,6 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%

View File

@ -1,6 +1,6 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%

View File

@ -1,6 +1,6 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%

View File

@ -1,6 +1,6 @@
/*-
* #%L
* HAPI FHIR - CDS Hooks
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -21,7 +21,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-caching-api</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
</dependency>
<dependency>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>hapi-deployable-pom</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -28,6 +28,7 @@ import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.RestfulServer;
@ -72,20 +73,29 @@ public class HapiFhirRepository implements Repository {
@Override
public <T extends IBaseResource, I extends IIdType> T read(
Class<T> theResourceType, I theId, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.READ)
.addHeaders(theHeaders)
.create();
return myDaoRegistry.getResourceDao(theResourceType).read(theId, details);
}
@Override
public <T extends IBaseResource> MethodOutcome create(T theResource, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.CREATE)
.addHeaders(theHeaders)
.create();
return myDaoRegistry.getResourceDao(theResource).create(theResource, details);
}
@Override
public <I extends IIdType, P extends IBaseParameters> MethodOutcome patch(
I theId, P thePatchParameters, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.PATCH)
.addHeaders(theHeaders)
.create();
// TODO update FHIR patchType once FHIRPATCH bug has been fixed
return myDaoRegistry
.getResourceDao(theId.getResourceType())
@ -94,7 +104,10 @@ public class HapiFhirRepository implements Repository {
@Override
public <T extends IBaseResource> MethodOutcome update(T theResource, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.UPDATE)
.addHeaders(theHeaders)
.create();
return myDaoRegistry.getResourceDao(theResource).update(theResource, details);
}
@ -102,7 +115,10 @@ public class HapiFhirRepository implements Repository {
@Override
public <T extends IBaseResource, I extends IIdType> MethodOutcome delete(
Class<T> theResourceType, I theId, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.DELETE)
.addHeaders(theHeaders)
.create();
return myDaoRegistry.getResourceDao(theResourceType).delete(theId, details);
}
@ -113,10 +129,14 @@ public class HapiFhirRepository implements Repository {
Class<T> theResourceType,
Map<String, List<IQueryParameterType>> theSearchParameters,
Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.SEARCH_TYPE)
.addHeaders(theHeaders)
.create();
SearchConverter converter = new SearchConverter();
converter.convertParameters(theSearchParameters, fhirContext());
details.setParameters(converter.resultParameters);
details.setResourceName(myRestfulServer.getFhirContext().getResourceType(theResourceType));
var bundleProvider =
myDaoRegistry.getResourceDao(theResourceType).search(converter.searchParameterMap, details);
@ -182,7 +202,10 @@ public class HapiFhirRepository implements Repository {
// repository action"?
@Override
public <B extends IBaseBundle> B link(Class<B> theBundleType, String theUrl, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.GET_PAGE)
.addHeaders(theHeaders)
.create();
var urlParts = UrlUtil.parseUrl(theUrl);
details.setCompleteUrl(theUrl);
details.setParameters(UrlUtil.parseQueryStrings(urlParts.getParams()));
@ -233,13 +256,19 @@ public class HapiFhirRepository implements Repository {
if (method == null) {
return null;
}
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.METADATA)
.addHeaders(theHeaders)
.create();
return (C) method.provideCapabilityStatement(myRestfulServer, details);
}
@Override
public <B extends IBaseBundle> B transaction(B theBundle, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.TRANSACTION)
.addHeaders(theHeaders)
.create();
return (B) myDaoRegistry.getSystemDao().transaction(details, theBundle);
}
@ -247,6 +276,7 @@ public class HapiFhirRepository implements Repository {
public <R extends IBaseResource, P extends IBaseParameters> R invoke(
String theName, P theParameters, Class<R> theReturnType, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER)
.addHeaders(theHeaders)
.setOperation(theName)
.setParameters(theParameters)
@ -259,6 +289,7 @@ public class HapiFhirRepository implements Repository {
public <P extends IBaseParameters> MethodOutcome invoke(
String theName, P theParameters, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER)
.addHeaders(theHeaders)
.setOperation(theName)
.setParameters(theParameters)
@ -275,6 +306,7 @@ public class HapiFhirRepository implements Repository {
Class<R> theReturnType,
Map<String, String> theHeaders) {
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER)
.addHeaders(theHeaders)
.setOperation(theName)
.setResourceType(theResourceType.getSimpleName())
@ -288,6 +320,7 @@ public class HapiFhirRepository implements Repository {
public <P extends IBaseParameters, T extends IBaseResource> MethodOutcome invoke(
Class<T> theResourceType, String theName, P theParameters, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER)
.addHeaders(theHeaders)
.setOperation(theName)
.setResourceType(theResourceType.getSimpleName())
@ -301,6 +334,7 @@ public class HapiFhirRepository implements Repository {
public <R extends IBaseResource, P extends IBaseParameters, I extends IIdType> R invoke(
I theId, String theName, P theParameters, Class<R> theReturnType, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER)
.addHeaders(theHeaders)
.setOperation(theName)
.setResourceType(theId.getResourceType())
@ -315,6 +349,7 @@ public class HapiFhirRepository implements Repository {
public <P extends IBaseParameters, I extends IIdType> MethodOutcome invoke(
I theId, String theName, P theParameters, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails)
.setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER)
.addHeaders(theHeaders)
.setOperation(theName)
.setResourceType(theId.getResourceType())

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.cr.repo;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import org.hl7.fhir.instance.model.api.IBaseParameters;
@ -56,6 +57,11 @@ class RequestDetailsCloner {
myDetails = theDetails;
}
DetailsBuilder setAction(RestOperationTypeEnum theRestOperationType) {
myDetails.setRestOperationType(theRestOperationType);
return this;
}
DetailsBuilder addHeaders(Map<String, String> theHeaders) {
if (theHeaders != null) {
for (var entry : theHeaders.entrySet()) {

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -149,8 +149,15 @@ public interface IIdHelperService<T extends IResourcePersistentId> {
@Nonnull
List<T> resolveResourcePersistentIdsWithCache(RequestPartitionId theRequestPartitionId, List<IIdType> theIds);
/**
* Value will be an empty Optional if the PID doesn't exist, or
* a typed resource ID if so (Patient/ABC).
*/
Optional<String> translatePidIdToForcedIdWithCache(T theResourcePersistentId);
/**
* Values in the returned map are typed resource IDs (Patient/ABC)
*/
PersistentIdToForcedIdMap<T> translatePidsToForcedIds(Set<T> theResourceIds);
/**

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.5-SNAPSHOT</version>
<version>7.7.6-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

Some files were not shown because too many files have changed in this diff Show More