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}