From 6226ae02d5310b38e5224338f379a0eb6d300746 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Thu, 18 Mar 2021 23:16:45 -0400 Subject: [PATCH 01/22] Add basic skeleton of MDM expansion --- .../ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java | 14 ++- .../fhir/jpa/dao/index/IdHelperService.java | 23 +++++ .../fhir/jpa/dao/mdm/MdmLinkExpandSvc.java | 88 +++++++++++++++++++ .../jpa/search/helper/SearchParamHelper.java | 60 +++++++++++++ .../fhir/jpa/term/BaseTermReadSvcImpl.java | 1 + .../validation/JpaValidationSupportChain.java | 1 + .../ca/uhn/fhir/jpa/config/TestR4Config.java | 3 +- .../ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java | 1 - .../fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java | 54 ++++++++++++ .../jpa/mdm/interceptor/MdmExpungeTest.java | 4 + .../uhn/fhir/jpa/config/TestJpaR4Config.java | 1 + 11 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/helper/SearchParamHelper.java 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 7ad98e46e11..28ea6cac850 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 @@ -33,11 +33,11 @@ import java.util.List; @Repository public interface IMdmLinkDao extends JpaRepository { @Modifying - @Query("DELETE FROM MdmLink f WHERE myGoldenResourcePid = :pid OR mySourcePid = :pid") + @Query("DELETE FROM MdmLink f WHERE f.myGoldenResourcePid = :pid OR f.mySourcePid = :pid") int deleteWithAnyReferenceToPid(@Param("pid") Long thePid); @Modifying - @Query("DELETE FROM MdmLink f WHERE (myGoldenResourcePid = :pid OR mySourcePid = :pid) AND myMatchResult <> :matchResult") + @Query("DELETE FROM MdmLink f WHERE (f.myGoldenResourcePid = :pid OR f.mySourcePid = :pid) AND f.myMatchResult <> :matchResult") int deleteWithAnyReferenceToPidAndMatchResultNot(@Param("pid") Long thePid, @Param("matchResult") MdmMatchResultEnum theMatchResult); @Query("SELECT ml2.myGoldenResourcePid, ml2.mySourcePid FROM MdmLink ml2 " + @@ -51,4 +51,14 @@ public interface IMdmLinkDao extends JpaRepository { "AND hrl.myTargetResourceType='Patient'" + ")") List> expandPidsFromGroupPidGivenMatchResult(@Param("groupPid") Long theGroupPid, @Param("matchResult") MdmMatchResultEnum theMdmMatchResultEnum); + + @Query("SELECT ml.myGoldenResourcePid, ml.mySourcePid " + + "FROM MdmLink ml " + + "INNER JOIN MdmLink ml2 " + + "on ml.myGoldenResourcePid=ml2.myGoldenResourcePid " + + "WHERE ml2.mySourcePid=:sourcePid " + + "AND ml2.myMatchResult=:matchResult " + + "AND ml.myMatchResult=:matchResult") + List> expandPidsBySourcePidAndMatchResult(@Param("sourcePid") Long theSourcePid, @Param("matchResult") MdmMatchResultEnum theMdmMatchResultEnum); + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java index a8795a47d4f..d28ba15b5d1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/index/IdHelperService.java @@ -380,6 +380,29 @@ public class IdHelperService { } } + /** + * + * Given a set of PIDs, return a set of public FHIR Resource IDs. + * This function will resolve a forced ID if it resolves, and if it fails to resolve to a forced it, will just return the pid + * Example: + * Let's say we have Patient/1(pid == 1), Patient/pat1 (pid == 2), Patient/3 (pid == 3), their pids would resolve as follows: + * + * [1,2,3] -> ["1","pat1","3"] + * + * @param thePids The Set of pids you would like to resolve to external FHIR Resource IDs. + * @return A Set of strings representing the FHIR IDs of the pids. + */ + public Set translatePidsToFhirResourceIds(Set thePids) { + Map> pidToForcedIdMap = translatePidsToForcedIds(thePids); + + //If the result of the translation is an empty optional, it means there is no forced id, and we can use the PID as the resource ID. + Set resolvedResourceIds = pidToForcedIdMap.entrySet().stream() + .map(entry -> entry.getValue().isPresent() ? entry.getValue().get() : entry.getKey().toString()) + .collect(Collectors.toSet()); + + return resolvedResourceIds; + + } public Map> translatePidsToForcedIds(Set thePids) { Map> retVal = new HashMap<>(myMemoryCacheService.getAllPresent(MemoryCacheService.CacheEnum.FORCED_ID, thePids)); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java new file mode 100644 index 00000000000..69d978725ae --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java @@ -0,0 +1,88 @@ +package ca.uhn.fhir.jpa.dao.mdm; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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% + */ + +import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao; +import ca.uhn.fhir.jpa.dao.index.IdHelperService; +import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +public class MdmLinkExpandSvc { + private static final Logger ourLog = LoggerFactory.getLogger(MdmLinkExpandSvc.class); + + @Autowired + private IMdmLinkDao myMdmLinkDao; + @Autowired + private IdHelperService myIdHelperService; + + /** + * Given a source resource, perform MDM expansion and return all the resource IDs of all resources that are + * MDM-Matched to this resource. + * + * @param theResource The resource to MDM-Expand + * @return A set of strings representing the FHIR IDs of the expanded resources. + */ + public Set expandMdmBySourceResource(IBaseResource theResource) { + return expandMdmBySourceResourceId(theResource.getIdElement()); + } + + /** + * Given a resource ID of a source resource, perform MDM expansion and return all the resource IDs of all resources that are + * MDM-Matched to this resource. + * + * @param theId The Resource ID of the resource to MDM-Expand + * @return A set of strings representing the FHIR ids of the expanded resources. + */ + public Set expandMdmBySourceResourceId(IIdType theId) { + Long pidOrThrowException = myIdHelperService.getPidOrThrowException(theId); + return expandMdmBySourceResourcePid(pidOrThrowException); + } + + /** + * Given a PID of a source resource, perform MDM expansion and return all the resource IDs of all resources that are + * MDM-Matched to this resource. + * + * @param theSourceResourcePid The PID of the resource to MDM-Expand + * @return A set of strings representing the FHIR ids of the expanded resources. + */ + public Set expandMdmBySourceResourcePid(Long theSourceResourcePid) { + List> goldenPidSourcePidTuples = myMdmLinkDao.expandPidsBySourcePidAndMatchResult(theSourceResourcePid, MdmMatchResultEnum.MATCH); + Set flattenedPids = new HashSet<>(); + goldenPidSourcePidTuples.forEach(flattenedPids::addAll); + + Set resourceIds = myIdHelperService.translatePidsToFhirResourceIds(flattenedPids); + return resourceIds; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/helper/SearchParamHelper.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/helper/SearchParamHelper.java new file mode 100644 index 00000000000..8b18d6faba8 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/helper/SearchParamHelper.java @@ -0,0 +1,60 @@ +package ca.uhn.fhir.jpa.search.helper; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.RuntimeSearchParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class SearchParamHelper { + + @Autowired + private FhirContext myFhirContext; + + + public Collection getPatientSearchParamsForResourceType(String theResourceType) { + RuntimeResourceDefinition runtimeResourceDefinition = myFhirContext.getResourceDefinition(theResourceType); + Map searchParams = new HashMap<>(); + + RuntimeSearchParam patientSearchParam = runtimeResourceDefinition.getSearchParam("patient"); + if (patientSearchParam != null) { + searchParams.put(patientSearchParam.getName(), patientSearchParam); + + } + RuntimeSearchParam subjectSearchParam = runtimeResourceDefinition.getSearchParam("subject"); + if (subjectSearchParam != null) { + searchParams.put(subjectSearchParam.getName(), subjectSearchParam); + } + + List compartmentSearchParams = getPatientCompartmentRuntimeSearchParams(runtimeResourceDefinition); + compartmentSearchParams.forEach(param -> searchParams.put(param.getName(), param)); + + return searchParams.values(); + } + + /** + * Search the resource definition for a compartment named 'patient' and return its related Search Parameter. + */ + public List getPatientCompartmentRuntimeSearchParams(RuntimeResourceDefinition runtimeResourceDefinition) { + List patientSearchParam = new ArrayList<>(); + List searchParams = runtimeResourceDefinition.getSearchParamsForCompartmentName("Patient"); + return searchParams; +// if (searchParams == null || searchParams.size() == 0) { +// String errorMessage = String.format("Resource type [%s] is not eligible for this type of export, as it contains no Patient compartment, and no `patient` or `subject` search parameter", myResourceType); +// throw new IllegalArgumentException(errorMessage); +// } else if (searchParams.size() == 1) { +// patientSearchParam = searchParams.get(0); +// } else { +// String errorMessage = String.format("Resource type [%s] is not eligible for Group Bulk export, as we are unable to disambiguate which patient search parameter we should be searching by.", myResourceType); +// throw new IllegalArgumentException(errorMessage); +// } +// return patientSearchParam; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java index 30526d363e0..687ec721349 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImpl.java @@ -2635,6 +2635,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc { @CoverageIgnore @Override public IValidationSupport.CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) { + //TODO GGG TRY TO JUST AUTO_PASS HERE AND SEE WHAT HAPPENS. invokeRunnableForUnitTest(); if (isNotBlank(theValueSetUrl)) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java index e3d50ce141a..eddfbacf81a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChain.java @@ -68,6 +68,7 @@ public class JpaValidationSupportChain extends ValidationSupportChain { public void postConstruct() { addValidationSupport(myDefaultProfileValidationSupport); addValidationSupport(myJpaValidationSupport); + //TODO MAKE SURE THAT THIS IS BEING CAL addValidationSupport(myTerminologyService); addValidationSupport(new SnapshotGeneratingValidationSupport(myFhirContext)); addValidationSupport(new InMemoryTerminologyServerValidationSupport(myFhirContext)); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java index d62a87b6818..222ecfe943c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java @@ -112,7 +112,8 @@ public class TestR4Config extends BaseJavaConfigR4 { }; retVal.setDriver(new org.h2.Driver()); - retVal.setUrl("jdbc:h2:mem:testdb_r4"); + retVal.setUrl("jdbc:h2:file:/home/tadgh/smile/hapi-fhir/testdb_r4.db"); +// retVal.setUrl("jdbc:h2:mem:testdb_r4"); retVal.setMaxWaitMillis(30000); retVal.setUsername(""); retVal.setPassword(""); diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java index 4245c859123..3c1b28764fa 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java @@ -182,7 +182,6 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test { Patient patient = (Patient) outcome.getResource(); patient.setId(outcome.getId()); return patient; - } @Nonnull diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java index 3d8c392e9a7..cd3978133b8 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java @@ -2,15 +2,26 @@ package ca.uhn.fhir.jpa.mdm.dao; import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; import ca.uhn.fhir.mdm.api.IMdmSettings; +import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; import ca.uhn.fhir.jpa.entity.MdmLink; import ca.uhn.fhir.jpa.util.TestUtil; +import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.in; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isIn; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -48,4 +59,47 @@ public class MdmLinkDaoSvcTest extends BaseMdmR4Test { assertEquals(rules.getVersion(), newLink.getVersion()); } + @Test + public void testExpandPidsWorks() { + + Patient golden = createGoldenPatient(); + + //Create 10 linked patients. + List mdmLinks = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + mdmLinks.add(createPatientAndLinkTo(golden.getIdElement().getIdPartAsLong(), MdmMatchResultEnum.MATCH)); + } + + //Now lets connect a few as just POSSIBLE_MATCHes and ensure they aren't returned. + for (int i = 0 ; i < 5; i++) { + createPatientAndLinkTo(golden.getIdElement().getIdPartAsLong(), MdmMatchResultEnum.POSSIBLE_MATCH); + } + + List expectedExpandedPids = mdmLinks.stream().map(MdmLink::getSourcePid).collect(Collectors.toList()); + + //SUT + List> lists = myMdmLinkDao.expandPidsBySourcePidAndMatchResult(mdmLinks.get(0).getSourcePid(), MdmMatchResultEnum.MATCH); + + assertThat(lists, hasSize(10)); + + lists.stream() + .forEach(pair -> { + assertThat(pair.get(0), is(equalTo(golden.getIdElement().getIdPartAsLong()))); + assertThat(pair.get(1), is(in(expectedExpandedPids))); + }); + } + + private MdmLink createPatientAndLinkTo(Long thePatientPid, MdmMatchResultEnum theMdmMatchResultEnum) { + Patient patient = createPatient(); + + MdmLink mdmLink = myMdmLinkDaoSvc.newMdmLink(); + mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL); + mdmLink.setMatchResult(theMdmMatchResultEnum); + mdmLink.setCreated(new Date()); + mdmLink.setUpdated(new Date()); + mdmLink.setGoldenResourcePid(thePatientPid); + mdmLink.setSourcePid(myIdHelperService.getPidOrNull(patient)); + MdmLink saved= myMdmLinkDao.save(mdmLink); + return saved; + } } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmExpungeTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmExpungeTest.java index a0ca44cc446..659222d3e2a 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmExpungeTest.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmExpungeTest.java @@ -16,6 +16,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.StringContains.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -49,6 +51,8 @@ public class MdmExpungeTest extends BaseMdmR4Test { saveLink(mdmLink); } + + @Test public void testUninterceptedDeleteRemovesMdmReference() { assertEquals(1, myMdmLinkDao.count()); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaR4Config.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaR4Config.java index 1716e099cad..d5c08620370 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaR4Config.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaR4Config.java @@ -68,6 +68,7 @@ public class TestJpaR4Config extends BaseJavaConfigR4 { retVal.setDriver(new org.h2.Driver()); retVal.setUrl("jdbc:h2:mem:testdb_r4"); +// retVal.setUrl("jdbc:h2:file:/home/tadgh/smile/hapi-fhir/testdb_r4.db"); retVal.setMaxWaitMillis(10000); retVal.setUsername(""); retVal.setPassword(""); From 659a8771003555d66c0e29c148eea3075b61bd8d Mon Sep 17 00:00:00 2001 From: Tadgh Date: Thu, 18 Mar 2021 23:17:56 -0400 Subject: [PATCH 02/22] Add interceptor --- ...SearchExpandingInterceptorInterceptor.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/MdmSearchExpandingInterceptorInterceptor.java 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 new file mode 100644 index 00000000000..9c61a0ad185 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/MdmSearchExpandingInterceptorInterceptor.java @@ -0,0 +1,105 @@ +package ca.uhn.fhir.jpa.interceptor; + +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * Copyright (C) 2014 - 2021 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% + */ + +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.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.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; + +/** + * This interceptor replaces the auto-generated CapabilityStatement that is generated + * by the HAPI FHIR Server with a static hard-coded resource. + */ +@Interceptor +public class MdmSearchExpandingInterceptorInterceptor { + private static final Logger ourLog = getLogger(MdmSearchExpandingInterceptorInterceptor.class); + + @Autowired + private MdmLinkExpandSvc myMdmLinkExpandSvc; + @Autowired + private SearchParamHelper mySearchParamHelper; + @Autowired + private FhirContext myFhirContext; + @Autowired + private IdHelperService myIdHelperService; + + + @Hook(Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH) + public boolean hook(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) { + Map parameters =theRequestDetails.getParameters(); + boolean shouldExpandMdm = false; + if (parameters.containsKey("_mdm")) { + shouldExpandMdm = parameters.get("_mdm").length == 1 && parameters.get("_mdm")[0].equalsIgnoreCase("true"); + } + + + if (shouldExpandMdm) { + String resourceName = theRequestDetails.getResourceName(); + Collection patientSearchParams = mySearchParamHelper.getPatientSearchParamsForResourceType(resourceName); + for (RuntimeSearchParam patientSearchParam: patientSearchParams) { + if (!theSearchParameterMap.containsKey(patientSearchParam.getName())) { + continue; + } + List> lists = theSearchParameterMap.get(patientSearchParam.getName()); + for (List list : lists) { + List toAdd = new ArrayList<>(); + for (IQueryParameterType paramVal : list) { + if (!paramVal.getMissing() && paramVal.getQueryParameterQualifier().equalsIgnoreCase("equals")){ + String valueAsQueryToken = paramVal.getValueAsQueryToken(myFhirContext); + Long pidOrThrowException = myIdHelperService.getPidOrThrowException(new IdDt(valueAsQueryToken)); + Set expandedIds= myMdmLinkExpandSvc.expandMdmBySourceResourcePid(pidOrThrowException); + ourLog.info("Expanded to resource ids: [{}]", String.join(",", expandedIds)); + toAdd.addAll(expandedIds.stream().map(StringParam::new).collect(Collectors.toList())); + } + } + list.addAll(toAdd); + } + } + } + return true; + } +} From e6cfb77c79f253542b70136ba4921de7b01c2bbd Mon Sep 17 00:00:00 2001 From: Tadgh Date: Tue, 23 Mar 2021 18:11:39 -0400 Subject: [PATCH 03/22] Wip --- .../src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java | 7 +++++++ .../java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java | 8 ++++++-- .../MdmSearchExpandingInterceptorInterceptor.java | 8 ++++---- .../test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java | 4 ++++ .../r4/FhirResourceDaoR4SearchCustomSearchParamTest.java | 3 +-- .../r4/ResourceProviderCustomSearchParamR4Test.java | 9 ++++++++- .../fhir/jpa/mdm/interceptor/MdmStorageInterceptor.java | 5 ++--- 7 files changed, 32 insertions(+), 12 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index 579ef42ee11..6ef6a72f468 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -59,6 +59,7 @@ import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.graphql.JpaStorageServices; import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor; import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices; +import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptorInterceptor; import ca.uhn.fhir.jpa.interceptor.OverridePathBasedReferentialIntegrityForDeletesInterceptor; import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder; import ca.uhn.fhir.jpa.model.sched.ISchedulerService; @@ -457,6 +458,12 @@ public abstract class BaseConfig { return new RequestTenantPartitionInterceptor(); } + @Bean + @Lazy + public MdmSearchExpandingInterceptorInterceptor mdmSearchExpandingInterceptorInterceptor() { + return new MdmSearchExpandingInterceptorInterceptor(); + } + @Bean @Lazy public TerminologyUploaderProvider terminologyUploaderProvider() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java index 69d978725ae..a04e4c2f795 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java @@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.dao.mdm; import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; +import ca.uhn.fhir.mdm.log.Logs; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -39,7 +40,7 @@ import java.util.Set; @Service public class MdmLinkExpandSvc { - private static final Logger ourLog = LoggerFactory.getLogger(MdmLinkExpandSvc.class); + private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); @Autowired private IMdmLinkDao myMdmLinkDao; @@ -54,6 +55,7 @@ public class MdmLinkExpandSvc { * @return A set of strings representing the FHIR IDs of the expanded resources. */ public Set expandMdmBySourceResource(IBaseResource theResource) { + ourLog.debug("About to MDM-expand source resource {}", theResource); return expandMdmBySourceResourceId(theResource.getIdElement()); } @@ -65,6 +67,7 @@ public class MdmLinkExpandSvc { * @return A set of strings representing the FHIR ids of the expanded resources. */ public Set expandMdmBySourceResourceId(IIdType theId) { + ourLog.debug("About to expand source resource with resource id {}", theId); Long pidOrThrowException = myIdHelperService.getPidOrThrowException(theId); return expandMdmBySourceResourcePid(pidOrThrowException); } @@ -77,11 +80,12 @@ public class MdmLinkExpandSvc { * @return A set of strings representing the FHIR ids of the expanded resources. */ public Set expandMdmBySourceResourcePid(Long theSourceResourcePid) { + ourLog.debug("About to expand source resource with PID {}", theSourceResourcePid); List> goldenPidSourcePidTuples = myMdmLinkDao.expandPidsBySourcePidAndMatchResult(theSourceResourcePid, MdmMatchResultEnum.MATCH); Set flattenedPids = new HashSet<>(); goldenPidSourcePidTuples.forEach(flattenedPids::addAll); - Set resourceIds = myIdHelperService.translatePidsToFhirResourceIds(flattenedPids); + ourLog.debug("Pid {} has been expanded to [{}]", theSourceResourcePid, String.join(",", resourceIds)); return resourceIds; } 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 9c61a0ad185..64762d566c2 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 @@ -29,6 +29,7 @@ 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; @@ -56,7 +57,7 @@ import static org.slf4j.LoggerFactory.getLogger; */ @Interceptor public class MdmSearchExpandingInterceptorInterceptor { - private static final Logger ourLog = getLogger(MdmSearchExpandingInterceptorInterceptor.class); + private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); @Autowired private MdmLinkExpandSvc myMdmLinkExpandSvc; @@ -67,7 +68,6 @@ public class MdmSearchExpandingInterceptorInterceptor { @Autowired private IdHelperService myIdHelperService; - @Hook(Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH) public boolean hook(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) { Map parameters =theRequestDetails.getParameters(); @@ -75,11 +75,11 @@ public class MdmSearchExpandingInterceptorInterceptor { if (parameters.containsKey("_mdm")) { shouldExpandMdm = parameters.get("_mdm").length == 1 && parameters.get("_mdm")[0].equalsIgnoreCase("true"); } - - if (shouldExpandMdm) { + ourLog.debug("Detected that incoming request has _mdm=true. The request was: {}", theRequestDetails.getRequestPath()); String resourceName = theRequestDetails.getResourceName(); Collection patientSearchParams = mySearchParamHelper.getPatientSearchParamsForResourceType(resourceName); + ourLog.debug("Resource type {} has patient search parameters [{}]", resourceName, patientSearchParams.stream().map(RuntimeSearchParam::getName).collect(Collectors.joining(", "))); for (RuntimeSearchParam patientSearchParam: patientSearchParams) { if (!theSearchParameterMap.containsKey(patientSearchParam.getName())) { continue; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index 4d36ee8c648..8e0b9adb277 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -129,6 +129,7 @@ import org.hl7.fhir.r4.model.DocumentReference; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence; import org.hl7.fhir.r4.model.EpisodeOfCare; +import org.hl7.fhir.r4.model.ExplanationOfBenefit; import org.hl7.fhir.r4.model.Group; import org.hl7.fhir.r4.model.Immunization; import org.hl7.fhir.r4.model.ImmunizationRecommendation; @@ -378,6 +379,9 @@ public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuil @Qualifier("myPatientDaoR4") protected IFhirResourceDaoPatient myPatientDao; @Autowired + @Qualifier("myExplanationOfBenefitDaoR4") + protected IFhirResourceDao myExplanationOfBenefitDao; + @Autowired protected IResourceTableDao myResourceTableDao; @Autowired protected IResourceHistoryTableDao myResourceHistoryTableDao; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java index 5e56111fa2d..7428da07671 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchCustomSearchParamTest.java @@ -38,6 +38,7 @@ import org.hl7.fhir.r4.model.DiagnosticReport; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; +import org.hl7.fhir.r4.model.ExplanationOfBenefit; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Group; import org.hl7.fhir.r4.model.IntegerType; @@ -193,10 +194,8 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam("lt0"))); assertEquals(0, search.size()); - } - /** * Draft search parameters should be ok even if they aren't completely valid */ diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java index 2da45eeda72..3b44e465d5e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java @@ -13,8 +13,11 @@ import ca.uhn.fhir.jpa.model.search.SearchStatusEnum; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.gclient.DateClientParam; import ca.uhn.fhir.rest.gclient.ReferenceClientParam; +import ca.uhn.fhir.rest.gclient.StringClientParam; import ca.uhn.fhir.rest.gclient.TokenClientParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.BundleUtil; import org.apache.commons.io.IOUtils; @@ -31,6 +34,7 @@ import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResource import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; +import org.hl7.fhir.r4.model.ExplanationOfBenefit; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; import org.hl7.fhir.r4.model.Patient; @@ -54,6 +58,8 @@ import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -328,9 +334,9 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide List foundResources = toUnqualifiedVersionlessIdValues(bundle); assertThat(foundResources, contains(p1id.getValue())); - } + @SuppressWarnings("unused") @Test public void testSearchQualifiedWithCustomReferenceParam() { @@ -416,6 +422,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide } + /** * See #1300 */ diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptor.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptor.java index 06704d4ff66..14dc07b62db 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptor.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptor.java @@ -36,6 +36,8 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.Patient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -57,13 +59,10 @@ public class MdmStorageInterceptor implements IMdmStorageInterceptor { private EIDHelper myEIDHelper; @Autowired private IMdmSettings myMdmSettings; - @Autowired - private GoldenResourceHelper myGoldenResourceHelper; @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED) public void blockManualResourceManipulationOnCreate(IBaseResource theBaseResource, RequestDetails theRequestDetails, ServletRequestDetails theServletRequestDetails) { - //If running in single EID mode, forbid multiple eids. if (myMdmSettings.isPreventMultipleEids()) { forbidIfHasMultipleEids(theBaseResource); From 9f13225aa5e6085f23c876fc388cb2d87a9225b1 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Fri, 26 Mar 2021 17:57:38 -0400 Subject: [PATCH 04/22] WIP interceptor using reference param modifier --- .../ca/uhn/fhir/interceptor/api/Pointcut.java | 6 ++- .../java/ca/uhn/fhir/rest/api/Constants.java | 1 + .../uhn/fhir/rest/param/ReferenceParam.java | 15 ++++++ .../ca/uhn/fhir/jpa/config/BaseConfig.java | 7 ++- ...SearchExpandingInterceptorInterceptor.java | 48 +++++-------------- .../jpa/search/SearchCoordinatorSvcImpl.java | 11 ++++- .../jpa/mdm/config/MdmConsumerConfig.java | 6 +++ .../MdmSubmitterInterceptorLoader.java | 4 ++ .../ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java | 3 ++ .../interceptor/MdmStorageInterceptorIT.java | 9 ++++ .../uhn/fhir/jpa/config/TestJpaR4Config.java | 1 - 11 files changed, 72 insertions(+), 39 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java index d5e9aa9a35b..837fc188567 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java @@ -1155,6 +1155,9 @@ public enum Pointcut implements IPointcut { * pulled out of the servlet request. This parameter is identical to the RequestDetails parameter above but will * only be populated when operating in a RestfulServer implementation. It is provided as a convenience. * + *
  • + * ca.uhn.fhir.jpa.searchparam.SearchParameterMap - Contains the details of the search being checked. This can be modified. + *
  • * *

    * Hooks should return void. @@ -1163,7 +1166,8 @@ public enum Pointcut implements IPointcut { STORAGE_PRESEARCH_REGISTERED(void.class, "ca.uhn.fhir.rest.server.util.ICachedSearchDetails", "ca.uhn.fhir.rest.api.server.RequestDetails", - "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails" + "ca.uhn.fhir.rest.server.servlet.ServletRequestDetails", + "ca.uhn.fhir.jpa.searchparam.SearchParameterMap" ), /** diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index 93d957299d2..ef960acc0a2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -200,6 +200,7 @@ public class Constants { public static final String PARAMQUALIFIER_STRING_CONTAINS = ":contains"; public static final String PARAMQUALIFIER_STRING_EXACT = ":exact"; public static final String PARAMQUALIFIER_TOKEN_TEXT = ":text"; + public static final String PARAMQUALIFIER_MDM = ":mdm"; public static final int STATUS_HTTP_200_OK = 200; public static final int STATUS_HTTP_201_CREATED = 201; public static final int STATUS_HTTP_204_NO_CONTENT = 204; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java index a8c43a708bd..583edaa6731 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.rest.param; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.util.CoverageIgnore; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -41,6 +42,7 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/ private String myBaseUrl; private String myValue; private String myIdPart; + private Boolean myMdmExpand; /** * Constructor @@ -121,6 +123,11 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/ @Override void doSetValueAsQueryToken(FhirContext theContext, String theParamName, String theQualifier, String theValue) { + if (Constants.PARAMQUALIFIER_MDM.equals(theQualifier)) { + myMdmExpand = true; + theQualifier = ""; + //TODO GGG i probably have to deal with chaining here? like refusing the mdm qualifier if i can detect its chained? + } String q = theQualifier; if (isNotBlank(q)) { if (q.startsWith(":")) { @@ -166,6 +173,14 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/ return myBaseUrl; } + public boolean isMdmExpand() { + return myMdmExpand != null && myMdmExpand; + } + + public ReferenceParam setMdmExpand(boolean theMdmExpand) { + myMdmExpand = theMdmExpand; + return this; + } public String getChain() { return myChain; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index 6ef6a72f468..55617d7b2a2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -40,6 +40,7 @@ import ca.uhn.fhir.jpa.dao.index.DaoResourceLinkResolver; import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor; +import ca.uhn.fhir.jpa.dao.mdm.MdmLinkExpandSvc; import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilder; import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderCoords; import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderDate; @@ -459,11 +460,15 @@ public abstract class BaseConfig { } @Bean - @Lazy public MdmSearchExpandingInterceptorInterceptor mdmSearchExpandingInterceptorInterceptor() { return new MdmSearchExpandingInterceptorInterceptor(); } + @Bean + public MdmLinkExpandSvc myMdmLinkExpandSvc() { + return new MdmLinkExpandSvc(); + } + @Bean @Lazy public TerminologyUploaderProvider terminologyUploaderProvider() { 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 64762d566c2..04333b578a7 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 @@ -35,6 +35,7 @@ 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; @@ -62,44 +63,21 @@ public class MdmSearchExpandingInterceptorInterceptor { @Autowired private MdmLinkExpandSvc myMdmLinkExpandSvc; @Autowired - private SearchParamHelper mySearchParamHelper; - @Autowired private FhirContext myFhirContext; @Autowired private IdHelperService myIdHelperService; - @Hook(Pointcut.STORAGE_PRECHECK_FOR_CACHED_SEARCH) - public boolean hook(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) { - Map parameters =theRequestDetails.getParameters(); - boolean shouldExpandMdm = false; - if (parameters.containsKey("_mdm")) { - shouldExpandMdm = parameters.get("_mdm").length == 1 && parameters.get("_mdm")[0].equalsIgnoreCase("true"); - } - if (shouldExpandMdm) { - ourLog.debug("Detected that incoming request has _mdm=true. The request was: {}", theRequestDetails.getRequestPath()); - String resourceName = theRequestDetails.getResourceName(); - Collection patientSearchParams = mySearchParamHelper.getPatientSearchParamsForResourceType(resourceName); - ourLog.debug("Resource type {} has patient search parameters [{}]", resourceName, patientSearchParams.stream().map(RuntimeSearchParam::getName).collect(Collectors.joining(", "))); - for (RuntimeSearchParam patientSearchParam: patientSearchParams) { - if (!theSearchParameterMap.containsKey(patientSearchParam.getName())) { - continue; - } - List> lists = theSearchParameterMap.get(patientSearchParam.getName()); - for (List list : lists) { - List toAdd = new ArrayList<>(); - for (IQueryParameterType paramVal : list) { - if (!paramVal.getMissing() && paramVal.getQueryParameterQualifier().equalsIgnoreCase("equals")){ - String valueAsQueryToken = paramVal.getValueAsQueryToken(myFhirContext); - Long pidOrThrowException = myIdHelperService.getPidOrThrowException(new IdDt(valueAsQueryToken)); - Set expandedIds= myMdmLinkExpandSvc.expandMdmBySourceResourcePid(pidOrThrowException); - ourLog.info("Expanded to resource ids: [{}]", String.join(",", expandedIds)); - toAdd.addAll(expandedIds.stream().map(StringParam::new).collect(Collectors.toList())); - } - } - list.addAll(toAdd); - } - } - } - return true; + @Hook(Pointcut.STORAGE_PRESEARCH_REGISTERED) + public void hook(RequestDetails theRequestDetails, SearchParameterMap theSearchParameterMap) { + System.out.println("zoop"); + theSearchParameterMap.values().stream() + .flatMap(Collection::stream) + .filter(queryParam -> queryParam instanceof ReferenceParam) + .filter(referenceParam -> ((ReferenceParam) referenceParam).isMdmExpand()) + .map(untypedParam -> (ReferenceParam)untypedParam) + .forEach(mdmReferenceParam -> { + System.out.println("zoop"); + System.out.println(mdmReferenceParam.toString()); + }); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java index 6786f9ff002..10a03f5c765 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java @@ -307,6 +307,13 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { ourLog.debug("Registering new search {}", searchUuid); + // Interceptor call: STORAGE_PRESEARCH_REGISTERED + HookParams params = new HookParams() + .add(ICachedSearchDetails.class, search) + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) + .add(SearchParameterMap.class, theParams); + JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params); Class resourceTypeClass = myContext.getResourceDefinition(theResourceType).getImplementingClass(); final ISearchBuilder sb = mySearchBuilderFactory.newSearchBuilder(theCallingDao, theResourceType, resourceTypeClass); sb.setFetchSize(mySyncSize); @@ -382,13 +389,15 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { private PersistedJpaSearchFirstPageBundleProvider submitSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequestDetails, String theSearchUuid, ISearchBuilder theSb, String theQueryString, RequestPartitionId theRequestPartitionId) { StopWatch w = new StopWatch(); Search search = new Search(); + //TODO GGG MOVE THIS POPULATE AND ALSO THE HOOK CALL HIGHER UP IN THE STACK. populateSearchEntity(theParams, theResourceType, theSearchUuid, theQueryString, search, theRequestPartitionId); // Interceptor call: STORAGE_PRESEARCH_REGISTERED HookParams params = new HookParams() .add(ICachedSearchDetails.class, search) .add(RequestDetails.class, theRequestDetails) - .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) + .add(SearchParameterMap.class, theParams); JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params); SearchTask task = new SearchTask(search, theCallingDao, theParams, theResourceType, theRequestDetails, theRequestPartitionId); diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java index 204b27e69f1..3ebb0dc6f4c 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.mdm.config; */ import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptorInterceptor; import ca.uhn.fhir.jpa.mdm.svc.MdmSurvivorshipSvcImpl; import ca.uhn.fhir.mdm.api.IMdmControllerSvc; import ca.uhn.fhir.mdm.api.IMdmExpungeSvc; @@ -79,6 +80,11 @@ public class MdmConsumerConfig { return new MdmStorageInterceptor(); } + @Bean + MdmSearchExpandingInterceptorInterceptor myMdmSearchExpandingInterceptorInterceptor() { + return new MdmSearchExpandingInterceptorInterceptor(); + } + @Bean IMdmSurvivorshipService mdmSurvivorshipService() { return new MdmSurvivorshipSvcImpl(); } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java index b3396377031..5d9360bb62d 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.mdm.interceptor; * #L% */ +import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptorInterceptor; import ca.uhn.fhir.mdm.api.IMdmSettings; import ca.uhn.fhir.mdm.log.Logs; import ca.uhn.fhir.interceptor.api.IInterceptorService; @@ -41,6 +42,8 @@ public class MdmSubmitterInterceptorLoader { @Autowired private IMdmStorageInterceptor myIMdmStorageInterceptor; @Autowired + private MdmSearchExpandingInterceptorInterceptor myMdmSearchExpandingInterceptorInterceptor; + @Autowired private IInterceptorService myInterceptorService; @Autowired private SubscriptionSubmitInterceptorLoader mySubscriptionSubmitInterceptorLoader; @@ -53,6 +56,7 @@ public class MdmSubmitterInterceptorLoader { myDaoConfig.addSupportedSubscriptionType(Subscription.SubscriptionChannelType.MESSAGE); myInterceptorService.registerInterceptor(myIMdmStorageInterceptor); + myInterceptorService.registerInterceptor(myMdmSearchExpandingInterceptorInterceptor); ourLog.info("MDM interceptor registered"); // We need to call SubscriptionSubmitInterceptorLoader.start() again in case there were no subscription types the first time it was called. mySubscriptionSubmitInterceptorLoader.start(); diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java index 3c1b28764fa..dae0782cccb 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java @@ -44,6 +44,7 @@ import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.ContactPoint; import org.hl7.fhir.r4.model.DateType; import org.hl7.fhir.r4.model.Medication; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; @@ -96,6 +97,8 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test { @Autowired protected IFhirResourceDao myPractitionerDao; @Autowired + protected IFhirResourceDao myObservationDao; + @Autowired protected MdmResourceMatcherSvc myMdmResourceMatcherSvc; @Autowired protected IMdmLinkDao myMdmLinkDao; diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java index 5ad4b9ebe84..051bf27c546 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java @@ -6,10 +6,12 @@ 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.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; +import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.server.TransactionLogMessages; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import org.hl7.fhir.instance.model.api.IAnyResource; @@ -65,6 +67,13 @@ public class MdmStorageInterceptorIT extends BaseMdmR4Test { assertLinkCount(1); } + @Test + public void testSearchExpandingInterceptorWorks() { + SearchParameterMap subject = new SearchParameterMap("subject", new ReferenceParam("Patient/123").setMdmExpand(true)).setLoadSynchronous(false); + myObservationDao.search(subject); + } + + @Test public void testDeleteGoldenResourceDeletesLinks() throws InterruptedException { myMdmHelper.createWithLatch(buildPaulPatient()); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaR4Config.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaR4Config.java index d5c08620370..1716e099cad 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaR4Config.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/config/TestJpaR4Config.java @@ -68,7 +68,6 @@ public class TestJpaR4Config extends BaseJavaConfigR4 { retVal.setDriver(new org.h2.Driver()); retVal.setUrl("jdbc:h2:mem:testdb_r4"); -// retVal.setUrl("jdbc:h2:file:/home/tadgh/smile/hapi-fhir/testdb_r4.db"); retVal.setMaxWaitMillis(10000); retVal.setUsername(""); retVal.setPassword(""); From e9e20988c969096fb7645992e754389227ca563e Mon Sep 17 00:00:00 2001 From: Tadgh Date: Mon, 29 Mar 2021 10:49:58 -0400 Subject: [PATCH 05/22] Partial moving of presearch regsitered pointcut --- .../jpa/search/SearchCoordinatorSvcImpl.java | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java index 10a03f5c765..aa1d16493f6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/SearchCoordinatorSvcImpl.java @@ -305,7 +305,10 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { public IBundleProvider registerSearch(final IFhirResourceDao theCallingDao, final SearchParameterMap theParams, String theResourceType, CacheControlDirective theCacheControlDirective, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) { final String searchUuid = UUID.randomUUID().toString(); + final String queryString = theParams.toNormalizedQueryString(myContext); ourLog.debug("Registering new search {}", searchUuid); + Search search = new Search(); + populateSearchEntity(theParams, theResourceType, searchUuid, queryString, search, theRequestPartitionId); // Interceptor call: STORAGE_PRESEARCH_REGISTERED HookParams params = new HookParams() @@ -334,7 +337,6 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { cacheStatus = SearchCacheStatusEnum.NOT_TRIED; } - final String queryString = theParams.toNormalizedQueryString(myContext); if (cacheStatus != SearchCacheStatusEnum.NOT_TRIED) { if (theParams.getEverythingMode() == null) { if (myDaoConfig.getReuseCachedSearchResultsForMillis() != null) { @@ -347,7 +349,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { } } - PersistedJpaSearchFirstPageBundleProvider retVal = submitSearch(theCallingDao, theParams, theResourceType, theRequestDetails, searchUuid, sb, queryString, theRequestPartitionId); + PersistedJpaSearchFirstPageBundleProvider retVal = submitSearch(theCallingDao, theParams, theResourceType, theRequestDetails, searchUuid, sb, queryString, theRequestPartitionId, search); retVal.setCacheStatus(cacheStatus); return retVal; @@ -386,25 +388,25 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc { } @NotNull - private PersistedJpaSearchFirstPageBundleProvider submitSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequestDetails, String theSearchUuid, ISearchBuilder theSb, String theQueryString, RequestPartitionId theRequestPartitionId) { + private PersistedJpaSearchFirstPageBundleProvider submitSearch(IDao theCallingDao, SearchParameterMap theParams, String theResourceType, RequestDetails theRequestDetails, String theSearchUuid, ISearchBuilder theSb, String theQueryString, RequestPartitionId theRequestPartitionId, Search theSearch) { StopWatch w = new StopWatch(); - Search search = new Search(); +// Search search = new Search(); //TODO GGG MOVE THIS POPULATE AND ALSO THE HOOK CALL HIGHER UP IN THE STACK. - populateSearchEntity(theParams, theResourceType, theSearchUuid, theQueryString, search, theRequestPartitionId); +// populateSearchEntity(theParams, theResourceType, theSearchUuid, theQueryString, search, theRequestPartitionId); - // Interceptor call: STORAGE_PRESEARCH_REGISTERED - HookParams params = new HookParams() - .add(ICachedSearchDetails.class, search) - .add(RequestDetails.class, theRequestDetails) - .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) - .add(SearchParameterMap.class, theParams); - JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params); +// Interceptor call: STORAGE_PRESEARCH_REGISTERED +// HookParams params = new HookParams() +// .add(ICachedSearchDetails.class, search) +// .add(RequestDetails.class, theRequestDetails) +// .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) +// .add(SearchParameterMap.class, theParams); +// JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESEARCH_REGISTERED, params); - SearchTask task = new SearchTask(search, theCallingDao, theParams, theResourceType, theRequestDetails, theRequestPartitionId); - myIdToSearchTask.put(search.getUuid(), task); + SearchTask task = new SearchTask(theSearch, theCallingDao, theParams, theResourceType, theRequestDetails, theRequestPartitionId); + myIdToSearchTask.put(theSearch.getUuid(), task); myExecutor.submit(task); - PersistedJpaSearchFirstPageBundleProvider retVal = myPersistedJpaBundleProviderFactory.newInstanceFirstPage(theRequestDetails, search, task, theSb); + PersistedJpaSearchFirstPageBundleProvider retVal = myPersistedJpaBundleProviderFactory.newInstanceFirstPage(theRequestDetails, theSearch, task, theSb); ourLog.debug("Search initial phase completed in {}ms", w.getMillis()); return retVal; From 5f42743f48e96df6854de1e8db7fe80574326ce4 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Mon, 29 Mar 2021 19:16:26 -0400 Subject: [PATCH 06/22] wip --- .../search/lastn/config/TestElasticsearchContainerHelper.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/config/TestElasticsearchContainerHelper.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/config/TestElasticsearchContainerHelper.java index ba85d2ee77f..8bb1ed88b05 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/config/TestElasticsearchContainerHelper.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/config/TestElasticsearchContainerHelper.java @@ -21,6 +21,4 @@ public class TestElasticsearchContainerHelper { return new ElasticsearchContainer(ELASTICSEARCH_IMAGE) .withStartupTimeout(Duration.of(300, SECONDS)); - } - } From 838ac890d31f3b5833ced1b872ad5a2e6df6a29d Mon Sep 17 00:00:00 2001 From: Tadgh Date: Mon, 29 Mar 2021 19:19:37 -0400 Subject: [PATCH 07/22] wip --- .../search/lastn/config/TestElasticsearchContainerHelper.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/config/TestElasticsearchContainerHelper.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/config/TestElasticsearchContainerHelper.java index 8bb1ed88b05..ba85d2ee77f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/config/TestElasticsearchContainerHelper.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/config/TestElasticsearchContainerHelper.java @@ -21,4 +21,6 @@ public class TestElasticsearchContainerHelper { return new ElasticsearchContainer(ELASTICSEARCH_IMAGE) .withStartupTimeout(Duration.of(300, SECONDS)); + } + } From a15ded71408607f541e38a6991132ef59c85b5b6 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Mon, 29 Mar 2021 21:12:03 -0400 Subject: [PATCH 08/22] Interceptor now correctly intercepts at the right point --- .../java/ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java | 9 +++++++++ .../java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java | 7 +++++-- .../MdmSearchExpandingInterceptorInterceptor.java | 10 ++++++---- .../r4/ResourceProviderCustomSearchParamR4Test.java | 2 ++ .../ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java | 9 +++++---- .../jpa/mdm/interceptor/MdmStorageInterceptorIT.java | 3 +-- 6 files changed, 28 insertions(+), 12 deletions(-) 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 53814c503b7..653c229727b 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,4 +58,13 @@ public interface IMdmLinkDao extends JpaRepository { Long getSourcePid(); } + @Query("SELECT ml.myGoldenResourcePid, ml.mySourcePid " + + "FROM MdmLink ml " + + "INNER JOIN MdmLink ml2 " + + "on ml.myGoldenResourcePid=ml2.myGoldenResourcePid " + + "WHERE ml2.mySourcePid=:sourcePid " + + "AND ml2.myMatchResult=:matchResult " + + "AND ml.myMatchResult=:matchResult") + List expandPidsBySourcePidAndMatchResult(@Param("sourcePid") Long theSourcePid, @Param("matchResult") MdmMatchResultEnum theMdmMatchResultEnum); + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java index a04e4c2f795..66af7f5a706 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkExpandSvc.java @@ -81,9 +81,12 @@ public class MdmLinkExpandSvc { */ public Set expandMdmBySourceResourcePid(Long theSourceResourcePid) { ourLog.debug("About to expand source resource with PID {}", theSourceResourcePid); - List> goldenPidSourcePidTuples = myMdmLinkDao.expandPidsBySourcePidAndMatchResult(theSourceResourcePid, MdmMatchResultEnum.MATCH); + List goldenPidSourcePidTuples = myMdmLinkDao.expandPidsBySourcePidAndMatchResult(theSourceResourcePid, MdmMatchResultEnum.MATCH); Set flattenedPids = new HashSet<>(); - goldenPidSourcePidTuples.forEach(flattenedPids::addAll); + goldenPidSourcePidTuples.forEach(tuple -> { + flattenedPids.add(tuple.getSourcePid()); + flattenedPids.add(tuple.getGoldenPid()); + }); Set resourceIds = myIdHelperService.translatePidsToFhirResourceIds(flattenedPids); ourLog.debug("Pid {} has been expanded to [{}]", theSourceResourcePid, String.join(",", resourceIds)); return resourceIds; 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 04333b578a7..46052fc1d52 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 @@ -72,12 +72,14 @@ public class MdmSearchExpandingInterceptorInterceptor { System.out.println("zoop"); theSearchParameterMap.values().stream() .flatMap(Collection::stream) - .filter(queryParam -> queryParam instanceof ReferenceParam) - .filter(referenceParam -> ((ReferenceParam) referenceParam).isMdmExpand()) + .flatMap(Collection::stream) + .filter(param -> param instanceof ReferenceParam) .map(untypedParam -> (ReferenceParam)untypedParam) + .filter(ReferenceParam::isMdmExpand) .forEach(mdmReferenceParam -> { - System.out.println("zoop"); - System.out.println(mdmReferenceParam.toString()); + 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. }); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java index 59b1d653154..3453eddc465 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderCustomSearchParamR4Test.java @@ -58,6 +58,8 @@ import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java index cd3978133b8..ae034af6fa7 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.mdm.dao; +import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao; import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; import ca.uhn.fhir.mdm.api.IMdmSettings; import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; @@ -78,14 +79,14 @@ public class MdmLinkDaoSvcTest extends BaseMdmR4Test { List expectedExpandedPids = mdmLinks.stream().map(MdmLink::getSourcePid).collect(Collectors.toList()); //SUT - List> lists = myMdmLinkDao.expandPidsBySourcePidAndMatchResult(mdmLinks.get(0).getSourcePid(), MdmMatchResultEnum.MATCH); + List lists = myMdmLinkDao.expandPidsBySourcePidAndMatchResult(mdmLinks.get(0).getSourcePid(), MdmMatchResultEnum.MATCH); assertThat(lists, hasSize(10)); lists.stream() - .forEach(pair -> { - assertThat(pair.get(0), is(equalTo(golden.getIdElement().getIdPartAsLong()))); - assertThat(pair.get(1), is(in(expectedExpandedPids))); + .forEach(tuple -> { + assertThat(tuple.getGoldenPid(), is(equalTo(golden.getIdElement().getIdPartAsLong()))); + assertThat(tuple.getSourcePid(), is(in(expectedExpandedPids))); }); } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java index 051bf27c546..c06f0db2c7f 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java @@ -69,11 +69,10 @@ public class MdmStorageInterceptorIT extends BaseMdmR4Test { @Test public void testSearchExpandingInterceptorWorks() { - SearchParameterMap subject = new SearchParameterMap("subject", new ReferenceParam("Patient/123").setMdmExpand(true)).setLoadSynchronous(false); + 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()); From 419a5829ea5cb2b958c0911d61750a2005a6bc83 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Tue, 30 Mar 2021 12:39:37 -0400 Subject: [PATCH 09/22] This shockingly works --- .../ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java | 2 +- ...SearchExpandingInterceptorInterceptor.java | 58 ++- .../MdmSearchExpandingInterceptorIT.java | 348 ++++++++++++++++++ 3 files changed, 377 insertions(+), 31 deletions(-) create mode 100644 hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSearchExpandingInterceptorIT.java 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); + } + +} From 02e65141768bfe614366e17599225007a86bd576 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Tue, 30 Mar 2021 12:57:09 -0400 Subject: [PATCH 10/22] Add modelconfig toggle --- .../fhir/jpa/model/entity/ModelConfig.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java index 9a70e967165..b327ffec14b 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java @@ -99,6 +99,7 @@ public class ModelConfig { private Map> myTypeToAutoVersionReferenceAtPaths = Collections.emptyMap(); private boolean myRespectVersionsForSearchIncludes; private boolean myIndexOnContainedResources = false; + private boolean myAllowMdmExpansion = false; /** * Constructor @@ -159,6 +160,24 @@ public class ModelConfig { return myAllowContainsSearches; } + /** + * If enabled, the server will support the use of :mdm search parameter qualifier on Reference Search Parameters. + * This Parameter Qualifier is HAPI-specific, and not defined anywhere in the FHIR specification. Using this qualifier + * will result in an MDM expansion being done on the reference, which will expand the search scope. For example, if Patient/1 + * is MDM-matched to Patient/2 and you execute the search: + * Observation?subject:mdm=Patient/1 , you will receive observations for both Patient/1 and Patient/2. + *

    + * Default is false + *

    + * @since 5.4.0 + */ + public boolean isAllowMdmExpansion() { + return myAllowMdmExpansion; + } + public void setAllowMdmExpansion(boolean theAllowMdmExpansion) { + myAllowMdmExpansion = theAllowMdmExpansion; + } + /** * If enabled, the server will support the use of :contains searches, * which are helpful but can have adverse effects on performance. From a044e8c4eb16fccc520c81dbd36316777af45feb Mon Sep 17 00:00:00 2001 From: Tadgh Date: Tue, 30 Mar 2021 12:59:29 -0400 Subject: [PATCH 11/22] Add a delegator in daoconfig --- .../ca/uhn/fhir/jpa/api/config/DaoConfig.java | 30 +++++++++++++++++++ .../fhir/jpa/model/entity/ModelConfig.java | 12 ++++++++ 2 files changed, 42 insertions(+) diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java index 8c0f27d6a8b..6353d0a55b9 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java @@ -1716,6 +1716,36 @@ public class DaoConfig { this.myModelConfig.setAllowContainsSearches(theAllowContainsSearches); } + /** + * If enabled, the server will support the use of :mdm search parameter qualifier on Reference Search Parameters. + * This Parameter Qualifier is HAPI-specific, and not defined anywhere in the FHIR specification. Using this qualifier + * will result in an MDM expansion being done on the reference, which will expand the search scope. For example, if Patient/1 + * is MDM-matched to Patient/2 and you execute the search: + * Observation?subject:mdm=Patient/1 , you will receive observations for both Patient/1 and Patient/2. + *

    + * Default is false + *

    + * @since 5.4.0 + */ + public boolean isAllowMdmExpansion() { + return myModelConfig.isAllowMdmExpansion(); + } + + /** + * If enabled, the server will support the use of :mdm search parameter qualifier on Reference Search Parameters. + * This Parameter Qualifier is HAPI-specific, and not defined anywhere in the FHIR specification. Using this qualifier + * will result in an MDM expansion being done on the reference, which will expand the search scope. For example, if Patient/1 + * is MDM-matched to Patient/2 and you execute the search: + * Observation?subject:mdm=Patient/1 , you will receive observations for both Patient/1 and Patient/2. + *

    + * Default is false + *

    + * @since 5.4.0 + */ + public void setAllowMdmExpansion(boolean theAllowMdmExpansion) { + myModelConfig.setAllowMdmExpansion(theAllowMdmExpansion); + } + /** * This setting may be used to advise the server that any references found in * resources that have any of the base URLs given here will be replaced with diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java index b327ffec14b..4101d2504b7 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ModelConfig.java @@ -174,6 +174,18 @@ public class ModelConfig { public boolean isAllowMdmExpansion() { return myAllowMdmExpansion; } + + /** + * If enabled, the server will support the use of :mdm search parameter qualifier on Reference Search Parameters. + * This Parameter Qualifier is HAPI-specific, and not defined anywhere in the FHIR specification. Using this qualifier + * will result in an MDM expansion being done on the reference, which will expand the search scope. For example, if Patient/1 + * is MDM-matched to Patient/2 and you execute the search: + * Observation?subject:mdm=Patient/1 , you will receive observations for both Patient/1 and Patient/2. + *

    + * Default is false + *

    + * @since 5.4.0 + */ public void setAllowMdmExpansion(boolean theAllowMdmExpansion) { myAllowMdmExpansion = theAllowMdmExpansion; } From 8b69d161bd7ede67a879b08246e44e654838ae2d Mon Sep 17 00:00:00 2001 From: Tadgh Date: Tue, 30 Mar 2021 17:12:55 -0400 Subject: [PATCH 12/22] Update docs --- .../uhn/fhir/rest/param/ReferenceParam.java | 2 + .../fhir/docs/server_jpa_mdm/mdm_expansion.md | 28 ++ .../ca/uhn/fhir/jpa/config/BaseConfig.java | 7 +- ...ava => MdmSearchExpandingInterceptor.java} | 23 +- .../jpa/mdm/config/MdmConsumerConfig.java | 6 +- .../MdmSubmitterInterceptorLoader.java | 4 +- .../MdmSearchExpandingInterceptorIT.java | 338 ++---------------- 7 files changed, 92 insertions(+), 316 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/{MdmSearchExpandingInterceptorInterceptor.java => MdmSearchExpandingInterceptor.java} (73%) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java index 583edaa6731..32cdaf427d0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java @@ -127,7 +127,9 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/ myMdmExpand = true; theQualifier = ""; //TODO GGG i probably have to deal with chaining here? like refusing the mdm qualifier if i can detect its chained? + //TODO GGG just throw an error if they try to chain } + String q = theQualifier; if (isNotBlank(q)) { if (q.startsWith(":")) { diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md new file mode 100644 index 00000000000..c4766cc71d1 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md @@ -0,0 +1,28 @@ +# MDM Expansion + +Once you have MDM enabled, and you have many linked resources, it can be useful to search across all resourcess. Let's say you have the following MDM links in your database: +``` +Patient/1 --> Patient/3 +Patient/2 --> Patient/3 +``` +This indicates that both Patient/1 and Patient/2 are MDM-mathed to the same golden resource (Patient/3). +What if you want to get all observations from Patient/1, but also include any observations from all of their linked resources. You could do this by first querying the [$mdm-query-links]() endpoint, and then making a subsequent call like the following +```http request +GET http://example.com:8000/Observation?subject=Patient/1,Patient/2,Patient/3 +``` + +But HAPI-FHIR allows a shorthand for this, by means of a Search Parameter qualifier, as follows: +```http request +GET http://example.com:8000/Observation?subject:mdm=Patient/1 +``` + +This `:mdm` parameter qualifier instructs an interceptor in HAPI fhir to expand the set of resources included in the search by their MDM-matched resources. The two above HTTP requests will return the same result. + +## Enabling MDM Expansion + +On top of needing to instantiate an MDM module, you must enable in the [DaoConfig](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html) bean, using the [Allow MDM Expansion](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html#setAllowMdmExpansion(boolean)) property. + +
    +It is important to note that enabling this functionality can lead to incorrect data being returned by a request, if your MDM links are incorrect. +
    + diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index 4d40bf04fde..3432af617ce 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -61,7 +61,7 @@ import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.graphql.JpaStorageServices; import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor; import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices; -import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptorInterceptor; +import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptor; import ca.uhn.fhir.jpa.interceptor.OverridePathBasedReferentialIntegrityForDeletesInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseTerminologyTranslationInterceptor; import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder; @@ -476,8 +476,9 @@ public abstract class BaseConfig { } @Bean - public MdmSearchExpandingInterceptorInterceptor mdmSearchExpandingInterceptorInterceptor() { - return new MdmSearchExpandingInterceptorInterceptor(); + @Lazy + public MdmSearchExpandingInterceptor mdmSearchExpandingInterceptorInterceptor() { + return new MdmSearchExpandingInterceptor(); } @Bean 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/MdmSearchExpandingInterceptor.java similarity index 73% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/MdmSearchExpandingInterceptorInterceptor.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/interceptor/MdmSearchExpandingInterceptor.java index d7fabc86763..63f3d3574bc 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/MdmSearchExpandingInterceptor.java @@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.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.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.dao.mdm.MdmLinkExpandSvc; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.mdm.log.Logs; @@ -30,6 +31,7 @@ import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.ReferenceParam; +import joptsimple.internal.Strings; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; @@ -44,17 +46,22 @@ import static org.slf4j.LoggerFactory.getLogger; * by the HAPI FHIR Server with a static hard-coded resource. */ @Interceptor -public class MdmSearchExpandingInterceptorInterceptor { +public class MdmSearchExpandingInterceptor { private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); @Autowired private MdmLinkExpandSvc myMdmLinkExpandSvc; + @Autowired + private DaoConfig myDaoConfig; + @Hook(Pointcut.STORAGE_PRESEARCH_REGISTERED) public void hook(SearchParameterMap theSearchParameterMap) { - for (List> andList : theSearchParameterMap.values()) { - for (List orList : andList) { - expandAnyReferenceParameters(orList); + if (myDaoConfig.isAllowMdmExpansion()) { + for (List> andList : theSearchParameterMap.values()) { + for (List orList : andList) { + expandAnyReferenceParameters(orList); + } } } } @@ -69,10 +76,12 @@ public class MdmSearchExpandingInterceptorInterceptor { if (iQueryParameterType instanceof ReferenceParam) { ReferenceParam refParam = (ReferenceParam) iQueryParameterType; if (refParam.isMdmExpand()) { - Set strings = myMdmLinkExpandSvc.expandMdmBySourceResourceId(new IdDt(refParam.getValue())); - if (!strings.isEmpty()) { + ourLog.debug("Found a reference parameter to expand: {}", refParam.toString()); + Set expandedResourceIds = myMdmLinkExpandSvc.expandMdmBySourceResourceId(new IdDt(refParam.getValue())); + if (!expandedResourceIds.isEmpty()) { + ourLog.debug("Parameter has been expanded to: {}", String.join(", ", expandedResourceIds)); toRemove.add(refParam); - strings.stream().map(resourceId -> new ReferenceParam(refParam.getResourceType() + "/" + resourceId)).forEach(toAdd::add); + expandedResourceIds.stream().map(resourceId -> new ReferenceParam(refParam.getResourceType() + "/" + resourceId)).forEach(toAdd::add); } } } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java index 3ebb0dc6f4c..03d4aacd491 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java @@ -21,7 +21,7 @@ package ca.uhn.fhir.jpa.mdm.config; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptorInterceptor; +import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptor; import ca.uhn.fhir.jpa.mdm.svc.MdmSurvivorshipSvcImpl; import ca.uhn.fhir.mdm.api.IMdmControllerSvc; import ca.uhn.fhir.mdm.api.IMdmExpungeSvc; @@ -81,8 +81,8 @@ public class MdmConsumerConfig { } @Bean - MdmSearchExpandingInterceptorInterceptor myMdmSearchExpandingInterceptorInterceptor() { - return new MdmSearchExpandingInterceptorInterceptor(); + MdmSearchExpandingInterceptor myMdmSearchExpandingInterceptorInterceptor() { + return new MdmSearchExpandingInterceptor(); } @Bean diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java index 5d9360bb62d..912d39331ea 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.mdm.interceptor; * #L% */ -import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptorInterceptor; +import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptor; import ca.uhn.fhir.mdm.api.IMdmSettings; import ca.uhn.fhir.mdm.log.Logs; import ca.uhn.fhir.interceptor.api.IInterceptorService; @@ -42,7 +42,7 @@ public class MdmSubmitterInterceptorLoader { @Autowired private IMdmStorageInterceptor myIMdmStorageInterceptor; @Autowired - private MdmSearchExpandingInterceptorInterceptor myMdmSearchExpandingInterceptorInterceptor; + private MdmSearchExpandingInterceptor myMdmSearchExpandingInterceptorInterceptor; @Autowired private IInterceptorService myInterceptorService; @Autowired 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 index 69a87e09de4..f1f5eeb2216 100644 --- 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 @@ -1,31 +1,17 @@ package ca.uhn.fhir.jpa.mdm.interceptor; +import ca.uhn.fhir.jpa.api.config.DaoConfig; 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.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Reference; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.slf4j.Logger; @@ -33,24 +19,9 @@ 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.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.slf4j.LoggerFactory.getLogger; @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) @@ -63,286 +34,51 @@ public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test { @Autowired public MdmHelperR4 myMdmHelper; @Autowired - private IdHelperService myIdHelperService; + private DaoConfig myDaoConfig; @Test - public void testCreatePractitioner() throws InterruptedException { + public void testReferenceExpansionWorks() throws InterruptedException { + myDaoConfig.setAllowMdmExpansion(false); 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")); + MdmHelperR4.OutcomeAndLogMessageWrapper withLatch1 = myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123")); + MdmHelperR4.OutcomeAndLogMessageWrapper withLatch2 = myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123")); + MdmHelperR4.OutcomeAndLogMessageWrapper withLatch3 = myMdmHelper.createWithLatch(addExternalEID(buildJanePatient(), "123")); + assertLinkCount(4); + String id = withLatch.getDaoMethodOutcome().getId().getIdPart(); + String id1 = withLatch1.getDaoMethodOutcome().getId().getIdPart(); + String id2 = withLatch2.getDaoMethodOutcome().getId().getIdPart(); + String id3 = withLatch3.getDaoMethodOutcome().getId().getIdPart(); + //Create an Observation for each Patient + createObservationWithSubject(id); + createObservationWithSubject(id1); + createObservationWithSubject(id2); + createObservationWithSubject(id3); 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())); + referenceOrListParam.addOr(new ReferenceParam("Patient/" + id).setMdmExpand(true)); 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")); + //With MDM Expansion disabled, this should return 1 result. + IBundleProvider search = myObservationDao.search(searchParameterMap); + assertThat(search.size(), is(equalTo(1))); - myObservationDao.search(searchParameterMap); - + //Once MDM Expansion is allowed, this should now return 4 resourecs. + myDaoConfig.setAllowMdmExpansion(true); + search = myObservationDao.search(searchParameterMap); + assertThat(search.size(), is(equalTo(4))); } - @Test - public void testSearchExpandingInterceptorWorks() { - SearchParameterMap subject = new SearchParameterMap("subject", new ReferenceParam("Patient/123").setMdmExpand(true)).setLoadSynchronous(true); - myObservationDao.search(subject); + private Observation createObservationWithSubject(String thePatientId) { + Observation observation = new Observation(); + observation.setSubject(new Reference("Patient/" + thePatientId)); + observation.setCode(new CodeableConcept().setText("Made for Patient/" + thePatientId)); + DaoMethodOutcome daoMethodOutcome = myObservationDao.create(observation); + return (Observation) daoMethodOutcome.getResource(); + } - - @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); - } - } From 007ce8fc6fc7370e26f1b8cb54dc752fd5d81a04 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Tue, 30 Mar 2021 23:40:20 -0400 Subject: [PATCH 13/22] add a test forthe case where there are no mdm links --- .../MdmSearchExpandingInterceptorIT.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 index f1f5eeb2216..58d3abf4afa 100644 --- 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 @@ -6,11 +6,13 @@ 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.api.MdmConstants; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.ReferenceOrListParam; import ca.uhn.fhir.rest.param.ReferenceParam; import org.hl7.fhir.r4.model.CodeableConcept; 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.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -73,6 +75,22 @@ public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test { assertThat(search.size(), is(equalTo(4))); } + @Test + public void testReferenceExpansionQuietlyFailsOnMissingMdmMatches() { + myDaoConfig.setAllowMdmExpansion(true); + Patient patient = buildJanePatient(); + patient.getMeta().addTag(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_NO_MDM_MANAGED, "Don't MDM on me!"); + DaoMethodOutcome daoMethodOutcome = myMdmHelper.doCreateResource(patient, true); + String id = daoMethodOutcome.getId().getIdPart(); + createObservationWithSubject(id); + + //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); + assertThat(search.size(), is(equalTo(1))); + } + private Observation createObservationWithSubject(String thePatientId) { Observation observation = new Observation(); observation.setSubject(new Reference("Patient/" + thePatientId)); From 696982ce1e2c12fc1912ddd9f0fff1b506405a47 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Wed, 31 Mar 2021 13:45:40 -0400 Subject: [PATCH 14/22] Fix bean name. Update docs --- .../ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md | 2 +- .../src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md index c4766cc71d1..6698087c463 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md @@ -6,7 +6,7 @@ Patient/1 --> Patient/3 Patient/2 --> Patient/3 ``` This indicates that both Patient/1 and Patient/2 are MDM-mathed to the same golden resource (Patient/3). -What if you want to get all observations from Patient/1, but also include any observations from all of their linked resources. You could do this by first querying the [$mdm-query-links]() endpoint, and then making a subsequent call like the following +What if you want to get all observations from Patient/1, but also include any observations from all of their linked resources. You could do this by first querying the [$mdm-query-links](/docs/server_jpa_mdm/mdm_operations.html) endpoint, and then making a subsequent call like the following ```http request GET http://example.com:8000/Observation?subject=Patient/1,Patient/2,Patient/3 ``` diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index 3432af617ce..437129e227d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -477,7 +477,7 @@ public abstract class BaseConfig { @Bean @Lazy - public MdmSearchExpandingInterceptor mdmSearchExpandingInterceptorInterceptor() { + public MdmSearchExpandingInterceptor mdmSearchExpandingInterceptor() { return new MdmSearchExpandingInterceptor(); } From 2ba3c919d58de0f2b6a76e2496473c84b9b26c5e Mon Sep 17 00:00:00 2001 From: Tadgh Date: Wed, 31 Mar 2021 14:18:57 -0400 Subject: [PATCH 15/22] Fix doGetQueryParameterQualifier on Mdm Expansion --- .../main/java/ca/uhn/fhir/rest/param/ReferenceParam.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java index 32cdaf427d0..765f25533f7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java @@ -92,8 +92,8 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/ } } - @Override - String doGetQueryParameterQualifier() { + + private String defaultGetQueryParameterQualifier() { StringBuilder b = new StringBuilder(); if (isNotBlank(myChain)) { if (isNotBlank(getResourceType())) { @@ -108,6 +108,10 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/ } return null; } + @Override + String doGetQueryParameterQualifier() { + return this.myMdmExpand != null ? ":mdm" : defaultGetQueryParameterQualifier(); + } @Override String doGetValueAsQueryToken(FhirContext theContext) { From f3c706d6564e220aeee4cf8b5bc04788cd83b3b0 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Wed, 31 Mar 2021 15:33:03 -0400 Subject: [PATCH 16/22] Add page for mdm expansion --- .../src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java | 2 -- .../src/main/resources/ca/uhn/hapi/fhir/docs/files.properties | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java index 765f25533f7..f30036a390b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceParam.java @@ -130,8 +130,6 @@ public class ReferenceParam extends BaseParam /*implements IQueryParameterType*/ if (Constants.PARAMQUALIFIER_MDM.equals(theQualifier)) { myMdmExpand = true; theQualifier = ""; - //TODO GGG i probably have to deal with chaining here? like refusing the mdm qualifier if i can detect its chained? - //TODO GGG just throw an error if they try to chain } String q = theQualifier; diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties index 6e4d038a21b..580cda79968 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties @@ -67,6 +67,7 @@ page.server_jpa_mdm.mdm_rules=MDM Rules page.server_jpa_mdm.mdm_eid=MDM Enterprise Identifiers page.server_jpa_mdm.mdm_operations=MDM Operations page.server_jpa_mdm.mdm_details=MDM Technical Details +page.server_jpa_mdm.mdm_expansion=MDM Search Expansion section.server_jpa_partitioning.title=JPA Server: Partitioning and Multitenancy page.server_jpa_partitioning.partitioning=Partitioning and Multitenancy From e5de34c51d8f32e2c7334f3263354a10fc4248c6 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Wed, 31 Mar 2021 16:08:09 -0400 Subject: [PATCH 17/22] Update docs --- .../ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md index 6698087c463..e4aa50a1989 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md @@ -1,7 +1,7 @@ # MDM Expansion -Once you have MDM enabled, and you have many linked resources, it can be useful to search across all resourcess. Let's say you have the following MDM links in your database: -``` +Once you have MDM enabled, and you have many linked resources, it can be useful to search across all linked resources. Let's say you have the following MDM links in your database: +```bash Patient/1 --> Patient/3 Patient/2 --> Patient/3 ``` @@ -18,9 +18,11 @@ GET http://example.com:8000/Observation?subject:mdm=Patient/1 This `:mdm` parameter qualifier instructs an interceptor in HAPI fhir to expand the set of resources included in the search by their MDM-matched resources. The two above HTTP requests will return the same result. +One important caveat is that chaining is currently not supported when using this prefix + ## Enabling MDM Expansion -On top of needing to instantiate an MDM module, you must enable in the [DaoConfig](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html) bean, using the [Allow MDM Expansion](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html#setAllowMdmExpansion(boolean)) property. +On top of needing to instantiate an MDM module, you must enable this feature in the [DaoConfig](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html) bean, using the [Allow MDM Expansion](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html#setAllowMdmExpansion(boolean)) property.
    It is important to note that enabling this functionality can lead to incorrect data being returned by a request, if your MDM links are incorrect. From d8866ec75f474d8382bc7deb0cc8cd6bd3e64ee4 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Wed, 31 Mar 2021 16:33:49 -0400 Subject: [PATCH 18/22] Add doc caveats --- .../ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md index e4aa50a1989..7acda791000 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md @@ -18,7 +18,7 @@ GET http://example.com:8000/Observation?subject:mdm=Patient/1 This `:mdm` parameter qualifier instructs an interceptor in HAPI fhir to expand the set of resources included in the search by their MDM-matched resources. The two above HTTP requests will return the same result. -One important caveat is that chaining is currently not supported when using this prefix +One important caveat is that chaining is currently not supported when using this prefix. ## Enabling MDM Expansion From 67fe940889fc771ec8d36d2513dfcb15f28335d9 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Thu, 1 Apr 2021 11:07:46 -0400 Subject: [PATCH 19/22] Update docs --- .../ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md index 7acda791000..51180269ded 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md @@ -18,13 +18,16 @@ GET http://example.com:8000/Observation?subject:mdm=Patient/1 This `:mdm` parameter qualifier instructs an interceptor in HAPI fhir to expand the set of resources included in the search by their MDM-matched resources. The two above HTTP requests will return the same result. + +
    One important caveat is that chaining is currently not supported when using this prefix. +
    ## Enabling MDM Expansion On top of needing to instantiate an MDM module, you must enable this feature in the [DaoConfig](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html) bean, using the [Allow MDM Expansion](/hapi-fhir/apidocs/hapi-fhir-jpaserver-api/ca/uhn/fhir/jpa/api/config/DaoConfig.html#setAllowMdmExpansion(boolean)) property. -
    -It is important to note that enabling this functionality can lead to incorrect data being returned by a request, if your MDM links are incorrect. +
    +It is important to note that enabling this functionality can lead to incorrect data being returned by a request, if your MDM links are incorrect. Use with caution.
    From 383c55e33995246b21894af42687f4c8ae5a9b0f Mon Sep 17 00:00:00 2001 From: Tadgh Date: Thu, 1 Apr 2021 11:33:45 -0400 Subject: [PATCH 20/22] Add changelog --- .../5_4_0/2520-support-mdm-expansion-in-search.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2520-support-mdm-expansion-in-search.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2520-support-mdm-expansion-in-search.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2520-support-mdm-expansion-in-search.yaml new file mode 100644 index 00000000000..8f619d91752 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2520-support-mdm-expansion-in-search.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 2520 +title: "Add support for `:mdm` search parameter qualifier on reference search parameters. Details about enabling this feature +can be found [in the documentation](/hapi-fhir/docs/server_jpa_mdm/mdm_expansion.html)." From 961c996ea98c0604d5f3e754f7fdabfe2f7f5322 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Thu, 1 Apr 2021 17:32:14 -0400 Subject: [PATCH 21/22] Remove foolish local bind --- .../src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java index 222ecfe943c..d62a87b6818 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java @@ -112,8 +112,7 @@ public class TestR4Config extends BaseJavaConfigR4 { }; retVal.setDriver(new org.h2.Driver()); - retVal.setUrl("jdbc:h2:file:/home/tadgh/smile/hapi-fhir/testdb_r4.db"); -// retVal.setUrl("jdbc:h2:mem:testdb_r4"); + retVal.setUrl("jdbc:h2:mem:testdb_r4"); retVal.setMaxWaitMillis(30000); retVal.setUsername(""); retVal.setPassword(""); From 82fc7b745781d78933465bc5a1a54fa14665b54e Mon Sep 17 00:00:00 2001 From: Tadgh Date: Fri, 9 Apr 2021 10:51:12 -0400 Subject: [PATCH 22/22] Fix typo --- .../ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md index 51180269ded..42302e38332 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_expansion.md @@ -5,7 +5,7 @@ Once you have MDM enabled, and you have many linked resources, it can be useful Patient/1 --> Patient/3 Patient/2 --> Patient/3 ``` -This indicates that both Patient/1 and Patient/2 are MDM-mathed to the same golden resource (Patient/3). +This indicates that both Patient/1 and Patient/2 are MDM-matched to the same golden resource (Patient/3). What if you want to get all observations from Patient/1, but also include any observations from all of their linked resources. You could do this by first querying the [$mdm-query-links](/docs/server_jpa_mdm/mdm_operations.html) endpoint, and then making a subsequent call like the following ```http request GET http://example.com:8000/Observation?subject=Patient/1,Patient/2,Patient/3