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