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