Add basic skeleton of MDM expansion

This commit is contained in:
Tadgh 2021-03-18 23:16:45 -04:00
parent 009f184ab4
commit 6226ae02d5
11 changed files with 246 additions and 4 deletions

View File

@ -33,11 +33,11 @@ import java.util.List;
@Repository
public interface IMdmLinkDao extends JpaRepository<MdmLink, Long> {
@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<MdmLink, Long> {
"AND hrl.myTargetResourceType='Patient'" +
")")
List<List<Long>> 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<List<Long>> expandPidsBySourcePidAndMatchResult(@Param("sourcePid") Long theSourcePid, @Param("matchResult") MdmMatchResultEnum theMdmMatchResultEnum);
}

View File

@ -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<String> translatePidsToFhirResourceIds(Set<Long> thePids) {
Map<Long, Optional<String>> 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<String> resolvedResourceIds = pidToForcedIdMap.entrySet().stream()
.map(entry -> entry.getValue().isPresent() ? entry.getValue().get() : entry.getKey().toString())
.collect(Collectors.toSet());
return resolvedResourceIds;
}
public Map<Long, Optional<String>> translatePidsToForcedIds(Set<Long> thePids) {
Map<Long, Optional<String>> retVal = new HashMap<>(myMemoryCacheService.getAllPresent(MemoryCacheService.CacheEnum.FORCED_ID, thePids));

View File

@ -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<String> 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<String> 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<String> expandMdmBySourceResourcePid(Long theSourceResourcePid) {
List<List<Long>> goldenPidSourcePidTuples = myMdmLinkDao.expandPidsBySourcePidAndMatchResult(theSourceResourcePid, MdmMatchResultEnum.MATCH);
Set<Long> flattenedPids = new HashSet<>();
goldenPidSourcePidTuples.forEach(flattenedPids::addAll);
Set<String> resourceIds = myIdHelperService.translatePidsToFhirResourceIds(flattenedPids);
return resourceIds;
}
}

View File

@ -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<RuntimeSearchParam> getPatientSearchParamsForResourceType(String theResourceType) {
RuntimeResourceDefinition runtimeResourceDefinition = myFhirContext.getResourceDefinition(theResourceType);
Map<String, RuntimeSearchParam> 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<RuntimeSearchParam> 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<RuntimeSearchParam> getPatientCompartmentRuntimeSearchParams(RuntimeResourceDefinition runtimeResourceDefinition) {
List<RuntimeSearchParam> patientSearchParam = new ArrayList<>();
List<RuntimeSearchParam> 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;
}
}

View File

@ -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)) {

View File

@ -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));

View File

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

View File

@ -182,7 +182,6 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test {
Patient patient = (Patient) outcome.getResource();
patient.setId(outcome.getId());
return patient;
}
@Nonnull

View File

@ -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<MdmLink> 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<Long> expectedExpandedPids = mdmLinks.stream().map(MdmLink::getSourcePid).collect(Collectors.toList());
//SUT
List<List<Long>> 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;
}
}

View File

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

View File

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