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.jpa.mdm.svc.candidate.CandidateList;
023import ca.uhn.fhir.jpa.mdm.svc.candidate.CandidateStrategyEnum;
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.blocklist.svc.IBlockRuleEvaluationSvc;
031import ca.uhn.fhir.mdm.log.Logs;
032import ca.uhn.fhir.mdm.model.MdmTransactionContext;
033import ca.uhn.fhir.mdm.util.GoldenResourceHelper;
034import ca.uhn.fhir.mdm.util.MdmResourceUtil;
035import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
036import ca.uhn.fhir.rest.server.TransactionLogMessages;
037import org.hl7.fhir.instance.model.api.IAnyResource;
038import org.slf4j.Logger;
039import org.springframework.beans.factory.annotation.Autowired;
040import org.springframework.stereotype.Service;
041import org.springframework.transaction.annotation.Transactional;
042
043import java.util.ArrayList;
044import java.util.List;
045
046/**
047 * MdmMatchLinkSvc is the entrypoint for HAPI's MDM system. An incoming resource can call
048 * updateMdmLinksForMdmSource and the underlying MDM system will take care of matching it to a GoldenResource,
049 * or creating a new GoldenResource if a suitable one was not found.
050 */
051@Service
052public class MdmMatchLinkSvc {
053
054        private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
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 MdmEidUpdateService myEidUpdateService;
067
068        @Autowired
069        private IBlockRuleEvaluationSvc myBlockRuleEvaluationSvc;
070
071        /**
072         * Given an MDM source (consisting of any supported MDM type), find a suitable Golden Resource candidate for them,
073         * or create one if one does not exist. Performs matching based on rules defined in mdm-rules.json.
074         * Does nothing if resource is determined to be not managed by MDM.
075         *
076         * @param theResource              the incoming MDM source, which can be any supported MDM type.
077         * @param theMdmTransactionContext
078         * @return an {@link TransactionLogMessages} which contains all informational messages related to MDM processing of this resource.
079         */
080        @Transactional
081        public MdmTransactionContext updateMdmLinksForMdmSource(
082                        IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) {
083                if (MdmResourceUtil.isMdmAllowed(theResource)) {
084                        return doMdmUpdate(theResource, theMdmTransactionContext);
085                } else {
086                        return null;
087                }
088        }
089
090        private MdmTransactionContext doMdmUpdate(
091                        IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) {
092                // we initialize to an empty list
093                // we require a candidatestrategy, but it doesn't matter
094                // because empty lists are effectively no matches
095                // (and so the candidate strategy doesn't matter)
096                CandidateList candidateList = new CandidateList(CandidateStrategyEnum.LINK);
097
098                /*
099                 * If a resource is blocked, we will not conduct
100                 * MDM matching. But we will still create golden resources
101                 * (so that future resources may match to it).
102                 */
103                boolean isResourceBlocked = myBlockRuleEvaluationSvc.isMdmMatchingBlocked(theResource);
104
105                if (!isResourceBlocked) {
106                        candidateList = myMdmGoldenResourceFindingSvc.findGoldenResourceCandidates(theResource);
107                }
108
109                if (isResourceBlocked || candidateList.isEmpty()) {
110                        handleMdmWithNoCandidates(theResource, theMdmTransactionContext);
111                } else if (candidateList.exactlyOneMatch()) {
112                        handleMdmWithSingleCandidate(theResource, candidateList.getOnlyMatch(), theMdmTransactionContext);
113                } else {
114                        handleMdmWithMultipleCandidates(theResource, candidateList, theMdmTransactionContext);
115                }
116                return theMdmTransactionContext;
117        }
118
119        private void handleMdmWithMultipleCandidates(
120                        IAnyResource theResource, CandidateList theCandidateList, MdmTransactionContext theMdmTransactionContext) {
121                MatchedGoldenResourceCandidate firstMatch = theCandidateList.getFirstMatch();
122                IResourcePersistentId<?> sampleGoldenResourcePid = firstMatch.getCandidateGoldenResourcePid();
123                boolean allSameGoldenResource = theCandidateList.stream()
124                                .allMatch(candidate -> candidate.getCandidateGoldenResourcePid().equals(sampleGoldenResourcePid));
125
126                if (allSameGoldenResource) {
127                        log(
128                                        theMdmTransactionContext,
129                                        "MDM received multiple match candidates, but they are all linked to the same Golden Resource.");
130                        handleMdmWithSingleCandidate(theResource, firstMatch, theMdmTransactionContext);
131                } else {
132                        log(
133                                        theMdmTransactionContext,
134                                        "MDM received multiple match candidates, that were linked to different Golden Resources. Setting POSSIBLE_DUPLICATES and POSSIBLE_MATCHES.");
135
136                        // Set them all as POSSIBLE_MATCH
137                        List<IAnyResource> goldenResources =
138                                        createPossibleMatches(theResource, theCandidateList, theMdmTransactionContext);
139
140                        // Set all GoldenResources as POSSIBLE_DUPLICATE of the last GoldenResource.
141                        IAnyResource firstGoldenResource = goldenResources.get(0);
142
143                        goldenResources.subList(1, goldenResources.size()).forEach(possibleDuplicateGoldenResource -> {
144                                MdmMatchOutcome outcome = MdmMatchOutcome.POSSIBLE_DUPLICATE;
145                                outcome.setEidMatch(theCandidateList.isEidMatch());
146                                myMdmLinkSvc.updateLink(
147                                                firstGoldenResource,
148                                                possibleDuplicateGoldenResource,
149                                                outcome,
150                                                MdmLinkSourceEnum.AUTO,
151                                                theMdmTransactionContext);
152                        });
153                }
154        }
155
156        private List<IAnyResource> createPossibleMatches(
157                        IAnyResource theResource, CandidateList theCandidateList, MdmTransactionContext theMdmTransactionContext) {
158                List<IAnyResource> goldenResources = new ArrayList<>();
159
160                for (MatchedGoldenResourceCandidate matchedGoldenResourceCandidate : theCandidateList.getCandidates()) {
161                        IAnyResource goldenResource =
162                                        myMdmGoldenResourceFindingSvc.getGoldenResourceFromMatchedGoldenResourceCandidate(
163                                                        matchedGoldenResourceCandidate, theMdmTransactionContext.getResourceType());
164
165                        MdmMatchOutcome outcome = new MdmMatchOutcome(
166                                                        matchedGoldenResourceCandidate.getMatchResult().getVector(),
167                                                        matchedGoldenResourceCandidate.getMatchResult().getScore())
168                                        .setMdmRuleCount(
169                                                        matchedGoldenResourceCandidate.getMatchResult().getMdmRuleCount());
170
171                        outcome.setMatchResultEnum(MdmMatchResultEnum.POSSIBLE_MATCH);
172                        outcome.setEidMatch(theCandidateList.isEidMatch());
173                        myMdmLinkSvc.updateLink(
174                                        goldenResource, theResource, outcome, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
175                        goldenResources.add(goldenResource);
176                }
177
178                return goldenResources;
179        }
180
181        private void handleMdmWithNoCandidates(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) {
182                log(
183                                theMdmTransactionContext,
184                                String.format(
185                                                "There were no matched candidates for MDM, creating a new %s Golden Resource.",
186                                                theResource.getIdElement().getResourceType()));
187                IAnyResource newGoldenResource =
188                                myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theResource, theMdmTransactionContext);
189                // TODO GGG :)
190                // 1. Get the right helper
191                // 2. Create source resource for the MDM source
192                // 3. UPDATE MDM LINK TABLE
193
194                myMdmLinkSvc.updateLink(
195                                newGoldenResource,
196                                theResource,
197                                MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH,
198                                MdmLinkSourceEnum.AUTO,
199                                theMdmTransactionContext);
200        }
201
202        private void handleMdmCreate(
203                        IAnyResource theTargetResource,
204                        MatchedGoldenResourceCandidate theGoldenResourceCandidate,
205                        MdmTransactionContext theMdmTransactionContext) {
206                IAnyResource goldenResource = myMdmGoldenResourceFindingSvc.getGoldenResourceFromMatchedGoldenResourceCandidate(
207                                theGoldenResourceCandidate, theMdmTransactionContext.getResourceType());
208
209                if (myGoldenResourceHelper.isPotentialDuplicate(goldenResource, theTargetResource)) {
210                        log(
211                                        theMdmTransactionContext,
212                                        "Duplicate detected based on the fact that both resources have different external EIDs.");
213                        IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(
214                                        theTargetResource, theMdmTransactionContext);
215
216                        myMdmLinkSvc.updateLink(
217                                        newGoldenResource,
218                                        theTargetResource,
219                                        MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH,
220                                        MdmLinkSourceEnum.AUTO,
221                                        theMdmTransactionContext);
222                        myMdmLinkSvc.updateLink(
223                                        newGoldenResource,
224                                        goldenResource,
225                                        MdmMatchOutcome.POSSIBLE_DUPLICATE,
226                                        MdmLinkSourceEnum.AUTO,
227                                        theMdmTransactionContext);
228                } else {
229                        log(theMdmTransactionContext, "MDM has narrowed down to one candidate for matching.");
230
231                        if (theGoldenResourceCandidate.isMatch()) {
232                                myGoldenResourceHelper.handleExternalEidAddition(
233                                                goldenResource, theTargetResource, theMdmTransactionContext);
234                                myEidUpdateService.applySurvivorshipRulesAndSaveGoldenResource(
235                                                theTargetResource, goldenResource, theMdmTransactionContext);
236                        }
237
238                        myMdmLinkSvc.updateLink(
239                                        goldenResource,
240                                        theTargetResource,
241                                        theGoldenResourceCandidate.getMatchResult(),
242                                        MdmLinkSourceEnum.AUTO,
243                                        theMdmTransactionContext);
244                }
245        }
246
247        private void handleMdmWithSingleCandidate(
248                        IAnyResource theResource,
249                        MatchedGoldenResourceCandidate theGoldenResourceCandidate,
250                        MdmTransactionContext theMdmTransactionContext) {
251                if (theMdmTransactionContext.getRestOperation().equals(MdmTransactionContext.OperationType.UPDATE_RESOURCE)) {
252                        log(theMdmTransactionContext, "MDM has narrowed down to one candidate for matching.");
253                        myEidUpdateService.handleMdmUpdate(theResource, theGoldenResourceCandidate, theMdmTransactionContext);
254                } else {
255                        handleMdmCreate(theResource, theGoldenResourceCandidate, theMdmTransactionContext);
256                }
257        }
258
259        private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) {
260                theMdmTransactionContext.addTransactionLogMessage(theMessage);
261                ourLog.debug(theMessage);
262        }
263}