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}