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