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}