001/*-
002 * #%L
003 * HAPI FHIR - Master Data Management
004 * %%
005 * Copyright (C) 2014 - 2024 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.mdm.rules.svc;
021
022import ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.i18n.Msg;
025import ca.uhn.fhir.mdm.api.IMdmSettings;
026import ca.uhn.fhir.mdm.api.MdmConstants;
027import ca.uhn.fhir.mdm.api.MdmMatchEvaluation;
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.rules.json.MdmFieldMatchJson;
032import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
033import ca.uhn.fhir.mdm.rules.matcher.IMatcherFactory;
034import com.google.common.annotations.VisibleForTesting;
035import org.hl7.fhir.instance.model.api.IBaseResource;
036import org.slf4j.Logger;
037import org.springframework.stereotype.Service;
038
039import java.util.ArrayList;
040import java.util.List;
041
042/**
043 * The MdmResourceComparator is in charge of performing actual comparisons between left and right records.
044 * It does so by calling individual comparators, and returning a vector based on the combination of
045 * field comparators that matched.
046 */
047@Service
048public class MdmResourceMatcherSvc {
049        private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
050
051        private final FhirContext myFhirContext;
052        private final IMatcherFactory myMatcherFactory;
053        private final List<MdmResourceFieldMatcher> myFieldMatchers = new ArrayList<>();
054
055        private MdmRulesJson myMdmRulesJson;
056
057        public MdmResourceMatcherSvc(
058                        FhirContext theFhirContext, IMatcherFactory theIMatcherFactory, IMdmSettings theMdmSettings) {
059                myFhirContext = theFhirContext;
060                myMatcherFactory = theIMatcherFactory;
061                myMdmRulesJson = theMdmSettings.getMdmRules();
062
063                addFieldMatchers();
064        }
065
066        private void addFieldMatchers() {
067                if (myMdmRulesJson == null) {
068                        throw new ConfigurationException(Msg.code(1521)
069                                        + "Failed to load MDM Rules.  If MDM is enabled, then MDM rules must be available in context.");
070                }
071                myFieldMatchers.clear();
072                for (MdmFieldMatchJson matchFieldJson : myMdmRulesJson.getMatchFields()) {
073                        myFieldMatchers.add(
074                                        new MdmResourceFieldMatcher(myFhirContext, myMatcherFactory, matchFieldJson, myMdmRulesJson));
075                }
076        }
077
078        /**
079         * Given two {@link IBaseResource}s, perform all comparisons on them to determine an {@link MdmMatchResultEnum}, indicating
080         * to what level the two resources are considered to be matching.
081         *
082         * @param theLeftResource  The first {@link IBaseResource}.
083         * @param theRightResource The second {@link IBaseResource}
084         * @return an {@link MdmMatchResultEnum} indicating the result of the comparison.
085         */
086        public MdmMatchOutcome getMatchResult(IBaseResource theLeftResource, IBaseResource theRightResource) {
087                return match(theLeftResource, theRightResource);
088        }
089
090        MdmMatchOutcome match(IBaseResource theLeftResource, IBaseResource theRightResource) {
091                MdmMatchOutcome matchResult = getMatchOutcome(theLeftResource, theRightResource);
092                MdmMatchResultEnum matchResultEnum = myMdmRulesJson.getMatchResult(matchResult.getVector());
093                matchResult.setMatchResultEnum(matchResultEnum);
094                if (ourLog.isDebugEnabled()) {
095                        ourLog.debug(
096                                        "{} {}: {}",
097                                        matchResult.getMatchResultEnum(),
098                                        theRightResource.getIdElement().toUnqualifiedVersionless(),
099                                        matchResult);
100                        if (ourLog.isTraceEnabled()) {
101                                ourLog.trace(
102                                                "Field matcher results:\n{}",
103                                                myMdmRulesJson.getDetailedFieldMatchResultWithSuccessInformation(matchResult.getVector()));
104                        }
105                }
106                return matchResult;
107        }
108
109        /**
110         * This function generates a `match vector`, which is a long representation of a binary string
111         * generated by the results of each of the given comparator matches. For example.
112         * start with a binary representation of the value 0 for long: 0000
113         * first_name matches, so the value `1` is bitwise-ORed to the current value (0) in right-most position.
114         * `0001`
115         * <p>
116         * Next, we look at the second field comparator, and see if it matches. If it does, we left-shift 1 by the index
117         * of the comparator, in this case also 1.
118         * `0010`
119         * <p>
120         * Then, we bitwise-or it with the current retval:
121         * 0001|0010 = 0011
122         * The binary string is now `0011`, which when you return it as a long becomes `3`.
123         */
124        private MdmMatchOutcome getMatchOutcome(IBaseResource theLeftResource, IBaseResource theRightResource) {
125                long vector = 0;
126                double score = 0.0;
127                int appliedRuleCount = 0;
128
129                // TODO GGG MDM: This grabs ALL comparators, not just the ones we care about (e.g. the ones for Medication)
130                String resourceType = myFhirContext.getResourceType(theLeftResource);
131
132                for (int i = 0; i < myFieldMatchers.size(); ++i) {
133                        // any that are not for the resourceType in question.
134                        MdmResourceFieldMatcher fieldComparator = myFieldMatchers.get(i);
135                        if (!isValidResourceType(resourceType, fieldComparator.getResourceType())) {
136                                ourLog.debug(
137                                                "Matcher {} is not valid for resource type: {}. Skipping it.",
138                                                fieldComparator.getName(),
139                                                resourceType);
140                                continue;
141                        }
142                        ourLog.trace(
143                                        "Matcher {} is valid for resource type: {}. Evaluating match.",
144                                        fieldComparator.getName(),
145                                        resourceType);
146                        MdmMatchEvaluation matchEvaluation = fieldComparator.match(theLeftResource, theRightResource);
147                        if (matchEvaluation.match) {
148                                vector |= (1L << i);
149                                ourLog.trace(
150                                                "Match: Successfully matched matcher {} with score {}. New vector: {}",
151                                                fieldComparator.getName(),
152                                                matchEvaluation.score,
153                                                vector);
154                                score += matchEvaluation.score;
155                        } else {
156                                ourLog.trace(
157                                                "No match: Matcher {} did not match (score: {}).",
158                                                fieldComparator.getName(),
159                                                matchEvaluation.score);
160                        }
161                        appliedRuleCount += 1;
162                }
163
164                MdmMatchOutcome retVal = new MdmMatchOutcome(vector, score);
165                retVal.setMdmRuleCount(appliedRuleCount);
166                return retVal;
167        }
168
169        private boolean isValidResourceType(String theResourceType, String theFieldComparatorType) {
170                return (theFieldComparatorType.equalsIgnoreCase(MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE)
171                                || theFieldComparatorType.equalsIgnoreCase(theResourceType));
172        }
173
174        @VisibleForTesting
175        public void setMdmRulesJson(MdmRulesJson theMdmRulesJson) {
176                myMdmRulesJson = theMdmRulesJson;
177                addFieldMatchers();
178        }
179}