001package ca.uhn.fhir.jpa.mdm.svc; 002 003/*- 004 * #%L 005 * HAPI FHIR JPA Server - Master Data Management 006 * %% 007 * Copyright (C) 2014 - 2023 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.jpa.mdm.svc.candidate.CandidateList; 024import ca.uhn.fhir.jpa.mdm.svc.candidate.MatchedGoldenResourceCandidate; 025import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc; 026import ca.uhn.fhir.mdm.api.IMdmLinkSvc; 027import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; 028import ca.uhn.fhir.mdm.api.MdmMatchOutcome; 029import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; 030import ca.uhn.fhir.mdm.log.Logs; 031import ca.uhn.fhir.mdm.model.MdmTransactionContext; 032import ca.uhn.fhir.mdm.util.GoldenResourceHelper; 033import ca.uhn.fhir.mdm.util.MdmResourceUtil; 034import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 035import ca.uhn.fhir.rest.server.TransactionLogMessages; 036import org.hl7.fhir.instance.model.api.IAnyResource; 037import org.slf4j.Logger; 038import org.springframework.beans.factory.annotation.Autowired; 039import org.springframework.stereotype.Service; 040import org.springframework.transaction.annotation.Transactional; 041 042import java.util.ArrayList; 043import java.util.List; 044 045/** 046 * MdmMatchLinkSvc is the entrypoint for HAPI's MDM system. An incoming resource can call 047 * updateMdmLinksForMdmSource and the underlying MDM system will take care of matching it to a GoldenResource, 048 * or creating a new GoldenResource if a suitable one was not found. 049 */ 050@Service 051public class MdmMatchLinkSvc { 052 053 private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); 054 055 @Autowired 056 private IMdmLinkSvc myMdmLinkSvc; 057 @Autowired 058 private MdmGoldenResourceFindingSvc myMdmGoldenResourceFindingSvc; 059 @Autowired 060 private GoldenResourceHelper myGoldenResourceHelper; 061 @Autowired 062 private MdmEidUpdateService myEidUpdateService; 063 064 /** 065 * Given an MDM source (consisting of any supported MDM type), find a suitable Golden Resource candidate for them, 066 * or create one if one does not exist. Performs matching based on rules defined in mdm-rules.json. 067 * Does nothing if resource is determined to be not managed by MDM. 068 * 069 * @param theResource the incoming MDM source, which can be any supported MDM type. 070 * @param theMdmTransactionContext 071 * @return an {@link TransactionLogMessages} which contains all informational messages related to MDM processing of this resource. 072 */ 073 @Transactional 074 public MdmTransactionContext updateMdmLinksForMdmSource(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) { 075 if (MdmResourceUtil.isMdmAllowed(theResource)) { 076 return doMdmUpdate(theResource, theMdmTransactionContext); 077 } else { 078 return null; 079 } 080 } 081 082 private MdmTransactionContext doMdmUpdate(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) { 083 CandidateList candidateList = myMdmGoldenResourceFindingSvc.findGoldenResourceCandidates(theResource); 084 085 if (candidateList.isEmpty()) { 086 handleMdmWithNoCandidates(theResource, theMdmTransactionContext); 087 } else if (candidateList.exactlyOneMatch()) { 088 handleMdmWithSingleCandidate(theResource, candidateList.getOnlyMatch(), theMdmTransactionContext); 089 } else { 090 handleMdmWithMultipleCandidates(theResource, candidateList, theMdmTransactionContext); 091 } 092 return theMdmTransactionContext; 093 } 094 095 private void handleMdmWithMultipleCandidates(IAnyResource theResource, CandidateList theCandidateList, MdmTransactionContext theMdmTransactionContext) { 096 MatchedGoldenResourceCandidate firstMatch = theCandidateList.getFirstMatch(); 097 IResourcePersistentId<?> sampleGoldenResourcePid = firstMatch.getCandidateGoldenResourcePid(); 098 boolean allSameGoldenResource = theCandidateList.stream() 099 .allMatch(candidate -> candidate.getCandidateGoldenResourcePid().equals(sampleGoldenResourcePid)); 100 101 if (allSameGoldenResource) { 102 log(theMdmTransactionContext, "MDM received multiple match candidates, but they are all linked to the same Golden Resource."); 103 handleMdmWithSingleCandidate(theResource, firstMatch, theMdmTransactionContext); 104 } else { 105 log(theMdmTransactionContext, "MDM received multiple match candidates, that were linked to different Golden Resources. Setting POSSIBLE_DUPLICATES and POSSIBLE_MATCHES."); 106 107 //Set them all as POSSIBLE_MATCH 108 List<IAnyResource> goldenResources = createPossibleMatches(theResource, theCandidateList, theMdmTransactionContext); 109 110 //Set all GoldenResources as POSSIBLE_DUPLICATE of the last GoldenResource. 111 IAnyResource firstGoldenResource = goldenResources.get(0); 112 113 goldenResources.subList(1, goldenResources.size()) 114 .forEach(possibleDuplicateGoldenResource -> { 115 MdmMatchOutcome outcome = MdmMatchOutcome.POSSIBLE_DUPLICATE; 116 outcome.setEidMatch(theCandidateList.isEidMatch()); 117 myMdmLinkSvc.updateLink(firstGoldenResource, possibleDuplicateGoldenResource, outcome, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); 118 }); 119 } 120 } 121 122 private List<IAnyResource> createPossibleMatches(IAnyResource theResource, CandidateList theCandidateList, MdmTransactionContext theMdmTransactionContext) { 123 List<IAnyResource> goldenResources = new ArrayList<>(); 124 125 for (MatchedGoldenResourceCandidate matchedGoldenResourceCandidate : theCandidateList.getCandidates()) { 126 IAnyResource goldenResource = myMdmGoldenResourceFindingSvc 127 .getGoldenResourceFromMatchedGoldenResourceCandidate(matchedGoldenResourceCandidate, theMdmTransactionContext.getResourceType()); 128 129 MdmMatchOutcome outcome = new MdmMatchOutcome(matchedGoldenResourceCandidate.getMatchResult().getVector(), 130 matchedGoldenResourceCandidate.getMatchResult().getScore()) 131 .setMdmRuleCount( matchedGoldenResourceCandidate.getMatchResult().getMdmRuleCount()); 132 133 outcome.setMatchResultEnum(MdmMatchResultEnum.POSSIBLE_MATCH); 134 outcome.setEidMatch(theCandidateList.isEidMatch()); 135 myMdmLinkSvc.updateLink(goldenResource, theResource, outcome, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); 136 goldenResources.add(goldenResource); 137 } 138 139 return goldenResources; 140 } 141 142 private void handleMdmWithNoCandidates(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) { 143 log(theMdmTransactionContext, String.format("There were no matched candidates for MDM, creating a new %s Golden Resource.", theResource.getIdElement().getResourceType())); 144 IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theResource, theMdmTransactionContext); 145 // TODO GGG :) 146 // 1. Get the right helper 147 // 2. Create source resource for the MDM source 148 // 3. UPDATE MDM LINK TABLE 149 150 myMdmLinkSvc.updateLink(newGoldenResource, theResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); 151 } 152 153 private void handleMdmCreate(IAnyResource theTargetResource, MatchedGoldenResourceCandidate theGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext) { 154 IAnyResource goldenResource = myMdmGoldenResourceFindingSvc.getGoldenResourceFromMatchedGoldenResourceCandidate(theGoldenResourceCandidate, theMdmTransactionContext.getResourceType()); 155 156 if (myGoldenResourceHelper.isPotentialDuplicate(goldenResource, theTargetResource)) { 157 log(theMdmTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs."); 158 IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theTargetResource, theMdmTransactionContext); 159 160 myMdmLinkSvc.updateLink(newGoldenResource, theTargetResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); 161 myMdmLinkSvc.updateLink(newGoldenResource, goldenResource, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); 162 } else { 163 log(theMdmTransactionContext, "MDM has narrowed down to one candidate for matching."); 164 165 if (theGoldenResourceCandidate.isMatch()) { 166 myGoldenResourceHelper.handleExternalEidAddition(goldenResource, theTargetResource, theMdmTransactionContext); 167 myEidUpdateService.applySurvivorshipRulesAndSaveGoldenResource(theTargetResource, goldenResource, theMdmTransactionContext); 168 } 169 170 myMdmLinkSvc.updateLink(goldenResource, theTargetResource, theGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext); 171 } 172 } 173 174 private void handleMdmWithSingleCandidate(IAnyResource theResource, MatchedGoldenResourceCandidate theGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext) { 175 if (theMdmTransactionContext.getRestOperation().equals(MdmTransactionContext.OperationType.UPDATE_RESOURCE)) { 176 log(theMdmTransactionContext, "MDM has narrowed down to one candidate for matching."); 177 myEidUpdateService.handleMdmUpdate(theResource, theGoldenResourceCandidate, theMdmTransactionContext); 178 } else { 179 handleMdmCreate(theResource, theGoldenResourceCandidate, theMdmTransactionContext); 180 } 181 } 182 183 private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) { 184 theMdmTransactionContext.addTransactionLogMessage(theMessage); 185 ourLog.debug(theMessage); 186 } 187}