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.dao.MdmLinkDaoSvc; 024import ca.uhn.fhir.jpa.mdm.svc.candidate.MatchedGoldenResourceCandidate; 025import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc; 026import ca.uhn.fhir.mdm.api.IMdmLink; 027import ca.uhn.fhir.mdm.api.IMdmLinkSvc; 028import ca.uhn.fhir.mdm.api.IMdmSettings; 029import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService; 030import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; 031import ca.uhn.fhir.mdm.api.MdmMatchOutcome; 032import ca.uhn.fhir.mdm.log.Logs; 033import ca.uhn.fhir.mdm.model.CanonicalEID; 034import ca.uhn.fhir.mdm.model.MdmTransactionContext; 035import ca.uhn.fhir.mdm.util.EIDHelper; 036import ca.uhn.fhir.mdm.util.GoldenResourceHelper; 037import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 038import org.hl7.fhir.instance.model.api.IAnyResource; 039import org.slf4j.Logger; 040import org.springframework.beans.factory.annotation.Autowired; 041import org.springframework.stereotype.Service; 042 043import javax.annotation.Nullable; 044import java.util.List; 045import java.util.Optional; 046 047@Service 048public class MdmEidUpdateService { 049 050 private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); 051 052 @Autowired 053 private MdmResourceDaoSvc myMdmResourceDaoSvc; 054 @Autowired 055 private IMdmLinkSvc myMdmLinkSvc; 056 @Autowired 057 private MdmGoldenResourceFindingSvc myMdmGoldenResourceFindingSvc; 058 @Autowired 059 private GoldenResourceHelper myGoldenResourceHelper; 060 @Autowired 061 private EIDHelper myEIDHelper; 062 @Autowired 063 private MdmLinkDaoSvc myMdmLinkDaoSvc; 064 @Autowired 065 private IMdmSettings myMdmSettings; 066 @Autowired 067 private IMdmSurvivorshipService myMdmSurvivorshipService; 068 069 void handleMdmUpdate(IAnyResource theTargetResource, MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext) { 070 MdmUpdateContext updateContext = new MdmUpdateContext(theMatchedGoldenResourceCandidate, theTargetResource); 071 myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(theTargetResource, updateContext.getMatchedGoldenResource(), theMdmTransactionContext); 072 073 if (updateContext.isRemainsMatchedToSameGoldenResource()) { 074 // Copy over any new external EIDs which don't already exist. 075 if (!updateContext.isIncomingResourceHasAnEid() || updateContext.isHasEidsInCommon()) { 076 //update to patient that uses internal EIDs only. 077 myMdmLinkSvc.updateLink(updateContext.getMatchedGoldenResource(), theTargetResource, theMatchedGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext); 078 } else if (!updateContext.isHasEidsInCommon()) { 079 handleNoEidsInCommon(theTargetResource, theMatchedGoldenResourceCandidate, theMdmTransactionContext, updateContext); 080 } 081 } else { 082 //This is a new linking scenario. we have to break the existing link and link to the new Golden Resource. For now, we create duplicate. 083 //updated patient has an EID that matches to a new candidate. Link them, and set the Golden Resources possible duplicates 084 linkToNewGoldenResourceAndFlagAsDuplicate(theTargetResource, theMatchedGoldenResourceCandidate.getMatchResult(), updateContext.getExistingGoldenResource(), updateContext.getMatchedGoldenResource(), theMdmTransactionContext); 085 086 myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(theTargetResource, updateContext.getMatchedGoldenResource(), theMdmTransactionContext); 087 myMdmResourceDaoSvc.upsertGoldenResource(updateContext.getMatchedGoldenResource(), theMdmTransactionContext.getResourceType()); 088 } 089 } 090 091 private void handleNoEidsInCommon(IAnyResource theResource, MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext, MdmUpdateContext theUpdateContext) { 092 // the user is simply updating their EID. We propagate this change to the GoldenResource. 093 //overwrite. No EIDS in common, but still same GoldenResource. 094 if (myMdmSettings.isPreventMultipleEids()) { 095 if (myMdmLinkDaoSvc.findMdmMatchLinksByGoldenResource(theUpdateContext.getMatchedGoldenResource()).size() <= 1) { // If there is only 0/1 link on the GoldenResource, we can safely overwrite the EID. 096 handleExternalEidOverwrite(theUpdateContext.getMatchedGoldenResource(), theResource, theMdmTransactionContext); 097 } else { // If the GoldenResource has multiple targets tied to it, we can't just overwrite the EID, so we split the GoldenResource. 098 createNewGoldenResourceAndFlagAsDuplicate(theResource, theMdmTransactionContext, theUpdateContext.getExistingGoldenResource()); 099 } 100 } else { 101 myGoldenResourceHelper.handleExternalEidAddition(theUpdateContext.getMatchedGoldenResource(), theResource, theMdmTransactionContext); 102 } 103 myMdmLinkSvc.updateLink(theUpdateContext.getMatchedGoldenResource(), theResource, theMatchedGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext); 104 } 105 106 private void handleExternalEidOverwrite(IAnyResource theGoldenResource, IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) { 107 List<CanonicalEID> eidFromResource = myEIDHelper.getExternalEid(theResource); 108 if (!eidFromResource.isEmpty()) { 109 myGoldenResourceHelper.overwriteExternalEids(theGoldenResource, eidFromResource); 110 } 111 } 112 113 private boolean candidateIsSameAsMdmLinkGoldenResource(IMdmLink theExistingMatchLink, MatchedGoldenResourceCandidate theGoldenResourceCandidate) { 114 return theExistingMatchLink.getGoldenResourcePersistenceId().equals(theGoldenResourceCandidate.getCandidateGoldenResourcePid()); 115 } 116 117 private void createNewGoldenResourceAndFlagAsDuplicate(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext, IAnyResource theOldGoldenResource) { 118 log(theMdmTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs."); 119 IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theResource, theMdmTransactionContext); 120 121 myMdmLinkSvc.updateLink(newGoldenResource, theResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); 122 myMdmLinkSvc.updateLink(newGoldenResource, theOldGoldenResource, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); 123 } 124 125 private void linkToNewGoldenResourceAndFlagAsDuplicate(IAnyResource theResource, MdmMatchOutcome theMatchResult, IAnyResource theOldGoldenResource, IAnyResource theNewGoldenResource, MdmTransactionContext theMdmTransactionContext) { 126 log(theMdmTransactionContext, "Changing a match link!"); 127 myMdmLinkSvc.deleteLink(theOldGoldenResource, theResource, theMdmTransactionContext); 128 myMdmLinkSvc.updateLink(theNewGoldenResource, theResource, theMatchResult, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); 129 log(theMdmTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs."); 130 myMdmLinkSvc.updateLink(theNewGoldenResource, theOldGoldenResource, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); 131 } 132 133 private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) { 134 theMdmTransactionContext.addTransactionLogMessage(theMessage); 135 ourLog.debug(theMessage); 136 } 137 138 public void applySurvivorshipRulesAndSaveGoldenResource(IAnyResource theTargetResource, IAnyResource theGoldenResource, MdmTransactionContext theMdmTransactionContext) { 139 myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(theTargetResource, theGoldenResource, theMdmTransactionContext); 140 myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmTransactionContext.getResourceType()); 141 } 142 143 /** 144 * Data class to hold context surrounding an update operation for an MDM target. 145 */ 146 class MdmUpdateContext { 147 148 private final boolean myHasEidsInCommon; 149 private final boolean myIncomingResourceHasAnEid; 150 private IAnyResource myExistingGoldenResource; 151 private boolean myRemainsMatchedToSameGoldenResource; 152 private final IAnyResource myMatchedGoldenResource; 153 154 public IAnyResource getMatchedGoldenResource() { 155 return myMatchedGoldenResource; 156 } 157 158 MdmUpdateContext(MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, IAnyResource theResource) { 159 final String resourceType = theResource.getIdElement().getResourceType(); 160 myMatchedGoldenResource = myMdmGoldenResourceFindingSvc.getGoldenResourceFromMatchedGoldenResourceCandidate(theMatchedGoldenResourceCandidate, resourceType); 161 162 myHasEidsInCommon = myEIDHelper.hasEidOverlap(myMatchedGoldenResource, theResource); 163 myIncomingResourceHasAnEid = !myEIDHelper.getExternalEid(theResource).isEmpty(); 164 165 Optional<? extends IMdmLink> theExistingMatchOrPossibleMatchLink = myMdmLinkDaoSvc.getMatchedOrPossibleMatchedLinkForSource(theResource); 166 myExistingGoldenResource = null; 167 168 if (theExistingMatchOrPossibleMatchLink.isPresent()) { 169 IMdmLink mdmLink = theExistingMatchOrPossibleMatchLink.get(); 170 IResourcePersistentId existingGoldenResourcePid = mdmLink.getGoldenResourcePersistenceId(); 171 myExistingGoldenResource = myMdmResourceDaoSvc.readGoldenResourceByPid(existingGoldenResourcePid, resourceType); 172 myRemainsMatchedToSameGoldenResource = candidateIsSameAsMdmLinkGoldenResource(mdmLink, theMatchedGoldenResourceCandidate); 173 } else { 174 myRemainsMatchedToSameGoldenResource = false; 175 } 176 } 177 178 public boolean isHasEidsInCommon() { 179 return myHasEidsInCommon; 180 } 181 182 public boolean isIncomingResourceHasAnEid() { 183 return myIncomingResourceHasAnEid; 184 } 185 186 @Nullable 187 public IAnyResource getExistingGoldenResource() { 188 return myExistingGoldenResource; 189 } 190 191 public boolean isRemainsMatchedToSameGoldenResource() { 192 return myRemainsMatchedToSameGoldenResource; 193 } 194 } 195}