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