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.i18n.Msg; 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 ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 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 java.util.List; 045import java.util.Optional; 046import javax.annotation.Nullable; 047 048@Service 049public class MdmEidUpdateService { 050 051 private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); 052 053 @Autowired 054 private MdmResourceDaoSvc myMdmResourceDaoSvc; 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 EIDHelper myEIDHelper; 067 068 @Autowired 069 private MdmLinkDaoSvc myMdmLinkDaoSvc; 070 071 @Autowired 072 private IMdmSettings myMdmSettings; 073 074 @Autowired 075 private IMdmSurvivorshipService myMdmSurvivorshipService; 076 077 void handleMdmUpdate( 078 IAnyResource theTargetResource, 079 MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, 080 MdmTransactionContext theMdmTransactionContext) { 081 MdmUpdateContext updateContext = new MdmUpdateContext(theMatchedGoldenResourceCandidate, theTargetResource); 082 myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource( 083 theTargetResource, updateContext.getMatchedGoldenResource(), theMdmTransactionContext); 084 085 if (updateContext.isRemainsMatchedToSameGoldenResource()) { 086 // Copy over any new external EIDs which don't already exist. 087 if (!updateContext.isIncomingResourceHasAnEid() || updateContext.isHasEidsInCommon()) { 088 // update to patient that uses internal EIDs only. 089 myMdmLinkSvc.updateLink( 090 updateContext.getMatchedGoldenResource(), 091 theTargetResource, 092 theMatchedGoldenResourceCandidate.getMatchResult(), 093 MdmLinkSourceEnum.AUTO, 094 theMdmTransactionContext); 095 } else if (!updateContext.isHasEidsInCommon()) { 096 handleNoEidsInCommon( 097 theTargetResource, theMatchedGoldenResourceCandidate, theMdmTransactionContext, updateContext); 098 } 099 } else { 100 // This is a new linking scenario. we have to break the existing link and link to the new Golden Resource. 101 // For now, we create duplicate. 102 // updated patient has an EID that matches to a new candidate. Link them, and set the Golden Resources 103 // possible duplicates 104 IAnyResource theOldGoldenResource = updateContext.getExistingGoldenResource(); 105 if (theOldGoldenResource == null) { 106 throw new InternalErrorException( 107 Msg.code(2362) 108 + "Old golden resource was null while updating MDM links with new golden resource. It is likely that a $mdm-clear was performed without a $mdm-submit. Link will not be updated."); 109 } else { 110 linkToNewGoldenResourceAndFlagAsDuplicate( 111 theTargetResource, 112 theMatchedGoldenResourceCandidate.getMatchResult(), 113 theOldGoldenResource, 114 updateContext.getMatchedGoldenResource(), 115 theMdmTransactionContext); 116 117 myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource( 118 theTargetResource, updateContext.getMatchedGoldenResource(), theMdmTransactionContext); 119 myMdmResourceDaoSvc.upsertGoldenResource( 120 updateContext.getMatchedGoldenResource(), theMdmTransactionContext.getResourceType()); 121 } 122 } 123 } 124 125 private void handleNoEidsInCommon( 126 IAnyResource theResource, 127 MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, 128 MdmTransactionContext theMdmTransactionContext, 129 MdmUpdateContext theUpdateContext) { 130 // the user is simply updating their EID. We propagate this change to the GoldenResource. 131 // overwrite. No EIDS in common, but still same GoldenResource. 132 if (myMdmSettings.isPreventMultipleEids()) { 133 if (myMdmLinkDaoSvc 134 .findMdmMatchLinksByGoldenResource(theUpdateContext.getMatchedGoldenResource()) 135 .size() 136 <= 1) { // If there is only 0/1 link on the GoldenResource, we can safely overwrite the EID. 137 handleExternalEidOverwrite( 138 theUpdateContext.getMatchedGoldenResource(), theResource, theMdmTransactionContext); 139 } else { // If the GoldenResource has multiple targets tied to it, we can't just overwrite the EID, so we 140 // split the GoldenResource. 141 createNewGoldenResourceAndFlagAsDuplicate( 142 theResource, theMdmTransactionContext, theUpdateContext.getExistingGoldenResource()); 143 } 144 } else { 145 myGoldenResourceHelper.handleExternalEidAddition( 146 theUpdateContext.getMatchedGoldenResource(), theResource, theMdmTransactionContext); 147 } 148 myMdmLinkSvc.updateLink( 149 theUpdateContext.getMatchedGoldenResource(), 150 theResource, 151 theMatchedGoldenResourceCandidate.getMatchResult(), 152 MdmLinkSourceEnum.AUTO, 153 theMdmTransactionContext); 154 } 155 156 private void handleExternalEidOverwrite( 157 IAnyResource theGoldenResource, IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) { 158 List<CanonicalEID> eidFromResource = myEIDHelper.getExternalEid(theResource); 159 if (!eidFromResource.isEmpty()) { 160 myGoldenResourceHelper.overwriteExternalEids(theGoldenResource, eidFromResource); 161 } 162 } 163 164 private boolean candidateIsSameAsMdmLinkGoldenResource( 165 IMdmLink theExistingMatchLink, MatchedGoldenResourceCandidate theGoldenResourceCandidate) { 166 return theExistingMatchLink 167 .getGoldenResourcePersistenceId() 168 .equals(theGoldenResourceCandidate.getCandidateGoldenResourcePid()); 169 } 170 171 private void createNewGoldenResourceAndFlagAsDuplicate( 172 IAnyResource theResource, 173 MdmTransactionContext theMdmTransactionContext, 174 IAnyResource theOldGoldenResource) { 175 log( 176 theMdmTransactionContext, 177 "Duplicate detected based on the fact that both resources have different external EIDs."); 178 IAnyResource newGoldenResource = 179 myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theResource, theMdmTransactionContext); 180 181 myMdmLinkSvc.updateLink( 182 newGoldenResource, 183 theResource, 184 MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, 185 MdmLinkSourceEnum.AUTO, 186 theMdmTransactionContext); 187 myMdmLinkSvc.updateLink( 188 newGoldenResource, 189 theOldGoldenResource, 190 MdmMatchOutcome.POSSIBLE_DUPLICATE, 191 MdmLinkSourceEnum.AUTO, 192 theMdmTransactionContext); 193 } 194 195 private void linkToNewGoldenResourceAndFlagAsDuplicate( 196 IAnyResource theResource, 197 MdmMatchOutcome theMatchResult, 198 IAnyResource theOldGoldenResource, 199 IAnyResource theNewGoldenResource, 200 MdmTransactionContext theMdmTransactionContext) { 201 log(theMdmTransactionContext, "Changing a match link!"); 202 myMdmLinkSvc.deleteLink(theOldGoldenResource, theResource, theMdmTransactionContext); 203 myMdmLinkSvc.updateLink( 204 theNewGoldenResource, theResource, theMatchResult, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); 205 log( 206 theMdmTransactionContext, 207 "Duplicate detected based on the fact that both resources have different external EIDs."); 208 myMdmLinkSvc.updateLink( 209 theNewGoldenResource, 210 theOldGoldenResource, 211 MdmMatchOutcome.POSSIBLE_DUPLICATE, 212 MdmLinkSourceEnum.AUTO, 213 theMdmTransactionContext); 214 } 215 216 private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) { 217 theMdmTransactionContext.addTransactionLogMessage(theMessage); 218 ourLog.debug(theMessage); 219 } 220 221 public void applySurvivorshipRulesAndSaveGoldenResource( 222 IAnyResource theTargetResource, 223 IAnyResource theGoldenResource, 224 MdmTransactionContext theMdmTransactionContext) { 225 myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource( 226 theTargetResource, theGoldenResource, theMdmTransactionContext); 227 myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmTransactionContext.getResourceType()); 228 } 229 230 /** 231 * Data class to hold context surrounding an update operation for an MDM target. 232 */ 233 class MdmUpdateContext { 234 235 private final boolean myHasEidsInCommon; 236 private final boolean myIncomingResourceHasAnEid; 237 private IAnyResource myExistingGoldenResource; 238 private boolean myRemainsMatchedToSameGoldenResource; 239 private final IAnyResource myMatchedGoldenResource; 240 241 public IAnyResource getMatchedGoldenResource() { 242 return myMatchedGoldenResource; 243 } 244 245 MdmUpdateContext(MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, IAnyResource theResource) { 246 final String resourceType = theResource.getIdElement().getResourceType(); 247 myMatchedGoldenResource = myMdmGoldenResourceFindingSvc.getGoldenResourceFromMatchedGoldenResourceCandidate( 248 theMatchedGoldenResourceCandidate, resourceType); 249 250 myHasEidsInCommon = myEIDHelper.hasEidOverlap(myMatchedGoldenResource, theResource); 251 myIncomingResourceHasAnEid = 252 !myEIDHelper.getExternalEid(theResource).isEmpty(); 253 254 Optional<? extends IMdmLink> theExistingMatchOrPossibleMatchLink = 255 myMdmLinkDaoSvc.getMatchedOrPossibleMatchedLinkForSource(theResource); 256 myExistingGoldenResource = null; 257 258 if (theExistingMatchOrPossibleMatchLink.isPresent()) { 259 IMdmLink mdmLink = theExistingMatchOrPossibleMatchLink.get(); 260 IResourcePersistentId existingGoldenResourcePid = mdmLink.getGoldenResourcePersistenceId(); 261 myExistingGoldenResource = 262 myMdmResourceDaoSvc.readGoldenResourceByPid(existingGoldenResourcePid, resourceType); 263 myRemainsMatchedToSameGoldenResource = 264 candidateIsSameAsMdmLinkGoldenResource(mdmLink, theMatchedGoldenResourceCandidate); 265 } else { 266 myRemainsMatchedToSameGoldenResource = false; 267 } 268 } 269 270 public boolean isHasEidsInCommon() { 271 return myHasEidsInCommon; 272 } 273 274 public boolean isIncomingResourceHasAnEid() { 275 return myIncomingResourceHasAnEid; 276 } 277 278 @Nullable 279 public IAnyResource getExistingGoldenResource() { 280 return myExistingGoldenResource; 281 } 282 283 public boolean isRemainsMatchedToSameGoldenResource() { 284 return myRemainsMatchedToSameGoldenResource; 285 } 286 } 287}