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}