diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java index 653c229727b..16834facbe2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java @@ -58,7 +58,7 @@ public interface IMdmLinkDao extends JpaRepository { Long getSourcePid(); } - @Query("SELECT ml.myGoldenResourcePid, ml.mySourcePid " + + @Query("SELECT ml.myGoldenResourcePid as goldenPid, ml.mySourcePid as sourcePid " + "FROM MdmLink ml " + "INNER JOIN MdmLink ml2 " + "on ml.myGoldenResourcePid=ml2.myGoldenResourcePid " + diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/MdmSearchExpandingInterceptorInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/MdmSearchExpandingInterceptorInterceptor.java index 46052fc1d52..d7fabc86763 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/MdmSearchExpandingInterceptorInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/MdmSearchExpandingInterceptorInterceptor.java @@ -20,35 +20,22 @@ package ca.uhn.fhir.jpa.interceptor; * #L% */ -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.interceptor.api.Hook; import ca.uhn.fhir.interceptor.api.Interceptor; import ca.uhn.fhir.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.mdm.MdmLinkExpandSvc; -import ca.uhn.fhir.jpa.search.helper.SearchParamHelper; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.mdm.log.Logs; -import ca.uhn.fhir.model.api.IQueryParameterAnd; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.ReferenceParam; -import ca.uhn.fhir.rest.param.StringParam; -import ca.uhn.fhir.util.ClasspathUtil; -import org.apache.commons.lang3.Validate; -import org.hl7.fhir.instance.model.api.IBaseConformance; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import static org.slf4j.LoggerFactory.getLogger; @@ -62,24 +49,35 @@ public class MdmSearchExpandingInterceptorInterceptor { @Autowired private MdmLinkExpandSvc myMdmLinkExpandSvc; - @Autowired - private FhirContext myFhirContext; - @Autowired - private IdHelperService myIdHelperService; @Hook(Pointcut.STORAGE_PRESEARCH_REGISTERED) - public void hook(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) { - System.out.println("zoop"); - theSearchParameterMap.values().stream() - .flatMap(Collection::stream) - .flatMap(Collection::stream) - .filter(param -> param instanceof ReferenceParam) - .map(untypedParam -> (ReferenceParam)untypedParam) - .filter(ReferenceParam::isMdmExpand) - .forEach(mdmReferenceParam -> { - Set strings = myMdmLinkExpandSvc.expandMdmBySourceResourceId(new IdDt(mdmReferenceParam.getValue())); - System.out.println(String.join(",", strings)); - //TODO in AM, start here with a test that actually has an expansion to expand against. - }); + public void hook(SearchParameterMap theSearchParameterMap) { + for (List> andList : theSearchParameterMap.values()) { + for (List orList : andList) { + expandAnyReferenceParameters(orList); + } + } + } + + /** + * If a Parameter is a reference parameter, and it has been set to expand MDM, perform the expansion. + */ + private void expandAnyReferenceParameters(List orList) { + List toRemove = new ArrayList<>(); + List toAdd = new ArrayList<>(); + for (IQueryParameterType iQueryParameterType : orList) { + if (iQueryParameterType instanceof ReferenceParam) { + ReferenceParam refParam = (ReferenceParam) iQueryParameterType; + if (refParam.isMdmExpand()) { + Set strings = myMdmLinkExpandSvc.expandMdmBySourceResourceId(new IdDt(refParam.getValue())); + if (!strings.isEmpty()) { + toRemove.add(refParam); + strings.stream().map(resourceId -> new ReferenceParam(refParam.getResourceType() + "/" + resourceId)).forEach(toAdd::add); + } + } + } + } + orList.removeAll(toRemove); + orList.addAll(toAdd); } } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSearchExpandingInterceptorIT.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSearchExpandingInterceptorIT.java new file mode 100644 index 00000000000..69a87e09de4 --- /dev/null +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSearchExpandingInterceptorIT.java @@ -0,0 +1,348 @@ +package ca.uhn.fhir.jpa.mdm.interceptor; + +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.BaseMdmR4Test; +import ca.uhn.fhir.jpa.mdm.helper.MdmHelperConfig; +import ca.uhn.fhir.jpa.mdm.helper.MdmHelperR4; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.mdm.model.CanonicalEID; +import ca.uhn.fhir.mdm.rules.config.MdmSettings; +import ca.uhn.fhir.model.dstu2.resource.Observation; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; +import ca.uhn.fhir.rest.param.ReferenceOrListParam; +import ca.uhn.fhir.rest.param.ReferenceParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.TransactionLogMessages; +import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; +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.Enumerations; +import org.hl7.fhir.r4.model.Medication; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.SearchParameter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; + +import java.util.Date; +import java.util.List; + +import static ca.uhn.fhir.mdm.api.MdmConstants.CODE_GOLDEN_RECORD; +import static ca.uhn.fhir.mdm.api.MdmConstants.CODE_GOLDEN_RECORD_REDIRECTED; +import static ca.uhn.fhir.mdm.api.MdmConstants.CODE_HAPI_MDM_MANAGED; +import static ca.uhn.fhir.mdm.api.MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS; +import static ca.uhn.fhir.mdm.api.MdmConstants.SYSTEM_MDM_MANAGED; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.slf4j.LoggerFactory.getLogger; + +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +@ContextConfiguration(classes = {MdmHelperConfig.class}) +public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test { + + private static final Logger ourLog = getLogger(MdmSearchExpandingInterceptorIT.class); + + @RegisterExtension + @Autowired + public MdmHelperR4 myMdmHelper; + @Autowired + private IdHelperService myIdHelperService; + + @Test + public void testCreatePractitioner() throws InterruptedException { + MdmHelperR4.OutcomeAndLogMessageWrapper withLatch = myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123")); + IIdType id = withLatch.getDaoMethodOutcome().getId(); + myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123")); + myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123")); + myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123")); + assertLinkCount(4); + + + + SearchParameterMap searchParameterMap = new SearchParameterMap(); + searchParameterMap.setLoadSynchronous(true); + ReferenceOrListParam referenceOrListParam = new ReferenceOrListParam(); + referenceOrListParam.addOr(new ReferenceParam(id.toVersionless()).setMdmExpand(true)); + referenceOrListParam.addOr(new ReferenceParam(id.toVersionless())); + referenceOrListParam.addOr(new ReferenceParam(id.toVersionless())); + searchParameterMap.add(Observation.SP_SUBJECT, referenceOrListParam); + searchParameterMap.add(Observation.SP_CATEGORY, new TokenParam("test-1", "test-2")); + searchParameterMap.add(Observation.SP_ENCOUNTER, new ReferenceParam("Encounter/abc")); + + + myObservationDao.search(searchParameterMap); + + } + + @Test + public void testSearchExpandingInterceptorWorks() { + SearchParameterMap subject = new SearchParameterMap("subject", new ReferenceParam("Patient/123").setMdmExpand(true)).setLoadSynchronous(true); + myObservationDao.search(subject); + } + + @Test + public void testDeleteGoldenResourceDeletesLinks() throws InterruptedException { + myMdmHelper.createWithLatch(buildPaulPatient()); + assertLinkCount(1); + Patient sourcePatient = getOnlyGoldenPatient(); + myPatientDao.delete(sourcePatient.getIdElement()); + assertLinkCount(0); + } + + @Test + public void testCreatePatientWithMdmTagForbidden() throws InterruptedException { + //Creating a golden resource with the MDM-MANAGED tag should fail + Patient patient = new Patient(); + patient.getMeta().addTag(SYSTEM_MDM_MANAGED, CODE_HAPI_MDM_MANAGED, "User is managed by MDM"); + try { + myMdmHelper.doCreateResource(patient, true); + fail(); + } catch (ForbiddenOperationException e) { + assertThat(e.getMessage(), startsWith("Cannot create or modify Resources that are managed by MDM.")); + } + } + + @Test + public void testCreatePatientWithGoldenRecordTagForbidden() throws InterruptedException { + Patient patient = new Patient(); + patient.getMeta().addTag(SYSTEM_GOLDEN_RECORD_STATUS, CODE_GOLDEN_RECORD, "Golden Record"); + try { + myMdmHelper.doCreateResource(patient, true); + fail(); + } catch (ForbiddenOperationException e) { + assertThat(e.getMessage(), startsWith("Cannot create or modify Resources that are managed by MDM.")); + } + } + + @Test + public void testCreateMedicationWithGoldenRecordRedirectTagForbidden() throws InterruptedException { + Medication medication = new Medication(); + medication.getMeta().addTag(SYSTEM_GOLDEN_RECORD_STATUS, CODE_GOLDEN_RECORD_REDIRECTED, "Golden Record"); + try { + myMdmHelper.doCreateResource(medication, true); + fail(); + } catch (ForbiddenOperationException e) { + assertThat(e.getMessage(), startsWith("Cannot create or modify Resources that are managed by MDM.")); + } + } + + @Test + public void testCreatingGoldenResourceWithInsufficentMDMAttributesIsNotMDMProcessed() throws InterruptedException { + myMdmHelper.doCreateResource(new Patient(), true); + assertLinkCount(0); + } + + @Test + public void testCreatingPatientWithOneOrMoreMatchingAttributesIsMDMProcessed() throws InterruptedException { + myMdmHelper.createWithLatch(buildPaulPatient()); + assertLinkCount(1); + } + + @Test + public void testCreateOrganizationWithMdmTagForbidden() throws InterruptedException { + //Creating a organization with the MDM-MANAGED tag should fail + Organization organization = new Organization(); + organization.getMeta().addTag(SYSTEM_MDM_MANAGED, CODE_HAPI_MDM_MANAGED, "User is managed by MDM"); + try { + myMdmHelper.doCreateResource(organization, true); + fail(); + } catch (ForbiddenOperationException e) { + assertThat(e.getMessage(), startsWith("Cannot create or modify Resources that are managed by MDM.")); + } + } + + @Test + public void testUpdateOrganizationWithMdmTagForbidden() throws InterruptedException { + //Creating a organization with the MDM-MANAGED tag should fail + Organization organization = new Organization(); + myMdmHelper.doCreateResource(organization, true); + organization.getMeta().addTag(SYSTEM_MDM_MANAGED, CODE_HAPI_MDM_MANAGED, "User is managed by MDM"); + try { + myMdmHelper.doUpdateResource(organization, true); + fail(); + } catch (ForbiddenOperationException e) { + assertEquals("The HAPI-MDM tag on a resource may not be changed once created.", e.getMessage()); + } + } + + @Test + public void testGoldenResourceRecordsManagedByMdmAllShareSameTag() throws InterruptedException { + myMdmHelper.createWithLatch(buildJanePatient()); + myMdmHelper.createWithLatch(buildPaulPatient()); + + //TODO GGG MDM: this test is out of date, since we now are using golden record Patients + IBundleProvider search = myPatientDao.search(buildGoldenResourceSearchParameterMap()); + List resources = search.getResources(0, search.size()); + + for (IBaseResource r : resources) { + assertThat(r.getMeta().getTag(SYSTEM_MDM_MANAGED, CODE_HAPI_MDM_MANAGED), is(notNullValue())); + } + } + + @Test + public void testNonMdmManagedGoldenResourceCannotHaveMdmManagedTagAddedToThem() { + // GoldenResource created manually. + Patient patient = new Patient(); + DaoMethodOutcome daoMethodOutcome = myMdmHelper.doCreateResource(patient, true); + assertNotNull(daoMethodOutcome.getId()); + + //Updating that patient to set them as MDM managed is not allowed. + patient.getMeta().addTag(SYSTEM_MDM_MANAGED, CODE_HAPI_MDM_MANAGED, "User is managed by MDM"); + try { + myMdmHelper.doUpdateResource(patient, true); + fail(); + } catch (ForbiddenOperationException e) { + assertEquals("The HAPI-MDM tag on a resource may not be changed once created.", e.getMessage()); + } + } + + @Test + public void testMdmManagedGoldenResourceCannotBeModifiedByGoldenResourceUpdateRequest() throws InterruptedException { + // When MDM is enabled, only the MDM system is allowed to modify GoldenResource links of GoldenResources with the MDM-MANAGED tag. + Patient patient = new Patient(); + IIdType patientId = myMdmHelper.createWithLatch(buildPaulPatient()).getDaoMethodOutcome().getId().toUnqualifiedVersionless(); + + patient.setId(patientId); + + // Updating a Golden Resource Patient who was created via MDM should fail. + MdmLink mdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(myIdHelperService.getPidOrNull(patient)).get(); + Long sourcePatientPid = mdmLink.getGoldenResourcePid(); + Patient goldenResourcePatient = (Patient) myPatientDao.readByPid(new ResourcePersistentId(sourcePatientPid)); + goldenResourcePatient.setGender(Enumerations.AdministrativeGender.MALE); + try { + myMdmHelper.doUpdateResource(goldenResourcePatient, true); + fail(); + } catch (ForbiddenOperationException e) { + assertThat(e.getMessage(), startsWith("Cannot create or modify Resources that are managed by MDM.")); + } + } + + @Test + public void testMdmPointcutReceivesTransactionLogMessages() throws InterruptedException { + MdmHelperR4.OutcomeAndLogMessageWrapper wrapper = myMdmHelper.createWithLatch(buildJanePatient()); + + TransactionLogMessages mdmTransactionLogMessages = wrapper.getLogMessages(); + + //There is no TransactionGuid here as there is no TransactionLog in this context. + assertThat(mdmTransactionLogMessages.getTransactionGuid(), is(nullValue())); + + List messages = mdmTransactionLogMessages.getValues(); + assertThat(messages.isEmpty(), is(false)); + } + + @Test + public void testWhenASingularPatientUpdatesExternalEidThatGoldenResourceEidIsUpdated() throws InterruptedException { + Patient jane = addExternalEID(buildJanePatient(), "some_eid"); + MdmHelperR4.OutcomeAndLogMessageWrapper latch = myMdmHelper.createWithLatch(jane); + jane.setId(latch.getDaoMethodOutcome().getId()); + clearExternalEIDs(jane); + jane = addExternalEID(jane, "some_new_eid"); + + MdmHelperR4.OutcomeAndLogMessageWrapper outcomeWrapper = myMdmHelper.updateWithLatch(jane); + IAnyResource patient = getGoldenResourceFromTargetResource(jane); + List externalEids = myEIDHelper.getExternalEid(patient); + assertThat(externalEids, hasSize(1)); + assertThat("some_new_eid", is(equalTo(externalEids.get(0).getValue()))); + } + + @Test + public void testWhenEidUpdatesAreDisabledForbidsUpdatesToEidsOnTargets() throws InterruptedException { + setPreventEidUpdates(true); + Patient jane = addExternalEID(buildJanePatient(), "some_eid"); + MdmHelperR4.OutcomeAndLogMessageWrapper latch = myMdmHelper.createWithLatch(jane); + jane.setId(latch.getDaoMethodOutcome().getId()); + clearExternalEIDs(jane); + jane = addExternalEID(jane, "some_new_eid"); + try { + myMdmHelper.doUpdateResource(jane, true); + fail(); + } catch (ForbiddenOperationException e) { + assertThat(e.getMessage(), is(equalTo("While running with EID updates disabled, EIDs may not be updated on source resources"))); + } + setPreventEidUpdates(false); + } + + @Test + public void testWhenMultipleEidsAreDisabledThatTheInterceptorRejectsCreatesWithThem() { + setPreventMultipleEids(true); + Patient patient = buildJanePatient(); + addExternalEID(patient, "123"); + addExternalEID(patient, "456"); + try { + myMdmHelper.doCreateResource(patient, true); + fail(); + } catch (ForbiddenOperationException e) { + assertThat(e.getMessage(), is(equalTo("While running with multiple EIDs disabled, source resources may have at most one EID."))); + } + + setPreventMultipleEids(false); + } + + @Test + public void testInterceptorHandlesNonMdmResources() { + setPreventEidUpdates(true); + + //Create some arbitrary resource. + SearchParameter fooSp = new SearchParameter(); + fooSp.setCode("foo"); + fooSp.addBase("Bundle"); + fooSp.setType(Enumerations.SearchParamType.REFERENCE); + fooSp.setTitle("FOO SP"); + fooSp.setExpression("Bundle.entry[0].resource.as(Composition).encounter"); + fooSp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL); + fooSp.setStatus(Enumerations.PublicationStatus.ACTIVE); + + myMdmHelper.doCreateResource(fooSp, true); + fooSp.setXpathUsage(SearchParameter.XPathUsageType.PHONETIC); + myMdmHelper.doUpdateResource(fooSp, true); + } + + @Test + public void testPatientsWithNoEIDCanBeUpdated() throws InterruptedException { + setPreventEidUpdates(true); + Patient p = buildPaulPatient(); + MdmHelperR4.OutcomeAndLogMessageWrapper wrapper = myMdmHelper.createWithLatch(p); + + p.setId(wrapper.getDaoMethodOutcome().getId()); + p.setBirthDate(new Date()); + myMdmHelper.updateWithLatch(p); + setPreventEidUpdates(false); + } + + @Test + public void testPatientsCanHaveEIDAddedInStrictMode() throws InterruptedException { + setPreventEidUpdates(true); + Patient p = buildPaulPatient(); + MdmHelperR4.OutcomeAndLogMessageWrapper messageWrapper = myMdmHelper.createWithLatch(p); + p.setId(messageWrapper.getDaoMethodOutcome().getId()); + addExternalEID(p, "external eid"); + myMdmHelper.updateWithLatch(p); + setPreventEidUpdates(false); + } + + private void setPreventEidUpdates(boolean thePrevent) { + ((MdmSettings) myMdmSettings).setPreventEidUpdates(thePrevent); + } + + private void setPreventMultipleEids(boolean thePrevent) { + ((MdmSettings) myMdmSettings).setPreventMultipleEids(thePrevent); + } + +}