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.svc.candidate.CandidateList;
024import ca.uhn.fhir.jpa.mdm.svc.candidate.MatchedGoldenResourceCandidate;
025import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc;
026import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
027import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
028import ca.uhn.fhir.mdm.api.MdmMatchOutcome;
029import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
030import ca.uhn.fhir.mdm.log.Logs;
031import ca.uhn.fhir.mdm.model.MdmTransactionContext;
032import ca.uhn.fhir.mdm.util.GoldenResourceHelper;
033import ca.uhn.fhir.mdm.util.MdmResourceUtil;
034import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
035import ca.uhn.fhir.rest.server.TransactionLogMessages;
036import org.hl7.fhir.instance.model.api.IAnyResource;
037import org.slf4j.Logger;
038import org.springframework.beans.factory.annotation.Autowired;
039import org.springframework.stereotype.Service;
040import org.springframework.transaction.annotation.Transactional;
041
042import java.util.ArrayList;
043import java.util.List;
044
045/**
046 * MdmMatchLinkSvc is the entrypoint for HAPI's MDM system. An incoming resource can call
047 * updateMdmLinksForMdmSource and the underlying MDM system will take care of matching it to a GoldenResource,
048 * or creating a new GoldenResource if a suitable one was not found.
049 */
050@Service
051public class MdmMatchLinkSvc {
052
053        private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
054
055        @Autowired
056        private IMdmLinkSvc myMdmLinkSvc;
057        @Autowired
058        private MdmGoldenResourceFindingSvc myMdmGoldenResourceFindingSvc;
059        @Autowired
060        private GoldenResourceHelper myGoldenResourceHelper;
061        @Autowired
062        private MdmEidUpdateService myEidUpdateService;
063
064        /**
065         * Given an MDM source (consisting of any supported MDM type), find a suitable Golden Resource candidate for them,
066         * or create one if one does not exist. Performs matching based on rules defined in mdm-rules.json.
067         * Does nothing if resource is determined to be not managed by MDM.
068         *
069         * @param theResource              the incoming MDM source, which can be any supported MDM type.
070         * @param theMdmTransactionContext
071         * @return an {@link TransactionLogMessages} which contains all informational messages related to MDM processing of this resource.
072         */
073        @Transactional
074        public MdmTransactionContext updateMdmLinksForMdmSource(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) {
075                if (MdmResourceUtil.isMdmAllowed(theResource)) {
076                        return doMdmUpdate(theResource, theMdmTransactionContext);
077                } else {
078                        return null;
079                }
080        }
081
082        private MdmTransactionContext doMdmUpdate(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) {
083                CandidateList candidateList = myMdmGoldenResourceFindingSvc.findGoldenResourceCandidates(theResource);
084
085                if (candidateList.isEmpty()) {
086                        handleMdmWithNoCandidates(theResource, theMdmTransactionContext);
087                } else if (candidateList.exactlyOneMatch()) {
088                        handleMdmWithSingleCandidate(theResource, candidateList.getOnlyMatch(), theMdmTransactionContext);
089                } else {
090                        handleMdmWithMultipleCandidates(theResource, candidateList, theMdmTransactionContext);
091                }
092                return theMdmTransactionContext;
093        }
094
095        private void handleMdmWithMultipleCandidates(IAnyResource theResource, CandidateList theCandidateList, MdmTransactionContext theMdmTransactionContext) {
096                MatchedGoldenResourceCandidate firstMatch = theCandidateList.getFirstMatch();
097                IResourcePersistentId<?> sampleGoldenResourcePid = firstMatch.getCandidateGoldenResourcePid();
098                boolean allSameGoldenResource = theCandidateList.stream()
099                        .allMatch(candidate -> candidate.getCandidateGoldenResourcePid().equals(sampleGoldenResourcePid));
100
101                if (allSameGoldenResource) {
102                        log(theMdmTransactionContext, "MDM received multiple match candidates, but they are all linked to the same Golden Resource.");
103                        handleMdmWithSingleCandidate(theResource, firstMatch, theMdmTransactionContext);
104                } else {
105                        log(theMdmTransactionContext, "MDM received multiple match candidates, that were linked to different Golden Resources. Setting POSSIBLE_DUPLICATES and POSSIBLE_MATCHES.");
106
107                        //Set them all as POSSIBLE_MATCH
108                        List<IAnyResource> goldenResources = createPossibleMatches(theResource, theCandidateList, theMdmTransactionContext);
109
110                        //Set all GoldenResources as POSSIBLE_DUPLICATE of the last GoldenResource.
111                        IAnyResource firstGoldenResource = goldenResources.get(0);
112
113                        goldenResources.subList(1, goldenResources.size())
114                                .forEach(possibleDuplicateGoldenResource -> {
115                                        MdmMatchOutcome outcome = MdmMatchOutcome.POSSIBLE_DUPLICATE;
116                                        outcome.setEidMatch(theCandidateList.isEidMatch());
117                                        myMdmLinkSvc.updateLink(firstGoldenResource, possibleDuplicateGoldenResource, outcome, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
118                                });
119                }
120        }
121
122        private List<IAnyResource> createPossibleMatches(IAnyResource theResource, CandidateList theCandidateList, MdmTransactionContext theMdmTransactionContext) {
123                List<IAnyResource> goldenResources = new ArrayList<>();
124
125                for (MatchedGoldenResourceCandidate matchedGoldenResourceCandidate : theCandidateList.getCandidates()) {
126                        IAnyResource goldenResource = myMdmGoldenResourceFindingSvc
127                                .getGoldenResourceFromMatchedGoldenResourceCandidate(matchedGoldenResourceCandidate, theMdmTransactionContext.getResourceType());
128
129                        MdmMatchOutcome outcome = new MdmMatchOutcome(matchedGoldenResourceCandidate.getMatchResult().getVector(),
130                                        matchedGoldenResourceCandidate.getMatchResult().getScore())
131                                .setMdmRuleCount( matchedGoldenResourceCandidate.getMatchResult().getMdmRuleCount());
132
133                        outcome.setMatchResultEnum(MdmMatchResultEnum.POSSIBLE_MATCH);
134                        outcome.setEidMatch(theCandidateList.isEidMatch());
135                        myMdmLinkSvc.updateLink(goldenResource, theResource, outcome, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
136                        goldenResources.add(goldenResource);
137                }
138
139                return goldenResources;
140        }
141
142        private void handleMdmWithNoCandidates(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) {
143                log(theMdmTransactionContext, String.format("There were no matched candidates for MDM, creating a new %s Golden Resource.", theResource.getIdElement().getResourceType()));
144                IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theResource, theMdmTransactionContext);
145                // TODO GGG :)
146                // 1. Get the right helper
147                // 2. Create source resource for the MDM source
148                // 3. UPDATE MDM LINK TABLE
149
150                myMdmLinkSvc.updateLink(newGoldenResource, theResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
151        }
152
153        private void handleMdmCreate(IAnyResource theTargetResource, MatchedGoldenResourceCandidate theGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext) {
154                IAnyResource goldenResource = myMdmGoldenResourceFindingSvc.getGoldenResourceFromMatchedGoldenResourceCandidate(theGoldenResourceCandidate, theMdmTransactionContext.getResourceType());
155
156                if (myGoldenResourceHelper.isPotentialDuplicate(goldenResource, theTargetResource)) {
157                        log(theMdmTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs.");
158                        IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theTargetResource, theMdmTransactionContext);
159
160                        myMdmLinkSvc.updateLink(newGoldenResource, theTargetResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
161                        myMdmLinkSvc.updateLink(newGoldenResource, goldenResource, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
162                } else {
163                        log(theMdmTransactionContext, "MDM has narrowed down to one candidate for matching.");
164
165                        if (theGoldenResourceCandidate.isMatch()) {
166                                myGoldenResourceHelper.handleExternalEidAddition(goldenResource, theTargetResource, theMdmTransactionContext);
167                                myEidUpdateService.applySurvivorshipRulesAndSaveGoldenResource(theTargetResource, goldenResource, theMdmTransactionContext);
168                        }
169
170                        myMdmLinkSvc.updateLink(goldenResource, theTargetResource, theGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
171                }
172        }
173
174        private void handleMdmWithSingleCandidate(IAnyResource theResource, MatchedGoldenResourceCandidate theGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext) {
175                if (theMdmTransactionContext.getRestOperation().equals(MdmTransactionContext.OperationType.UPDATE_RESOURCE)) {
176                        log(theMdmTransactionContext, "MDM has narrowed down to one candidate for matching.");
177                        myEidUpdateService.handleMdmUpdate(theResource, theGoldenResourceCandidate, theMdmTransactionContext);
178                } else {
179                        handleMdmCreate(theResource, theGoldenResourceCandidate, theMdmTransactionContext);
180                }
181        }
182
183        private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) {
184                theMdmTransactionContext.addTransactionLogMessage(theMessage);
185                ourLog.debug(theMessage);
186        }
187}