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("");