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.FhirContext; 023import ca.uhn.fhir.fhirpath.IFhirPath; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.mdm.api.MdmMatchEvaluation; 026import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson; 027import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson; 028import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; 029import ca.uhn.fhir.mdm.rules.json.MdmSimilarityJson; 030import ca.uhn.fhir.mdm.rules.matcher.IMatcherFactory; 031import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher; 032import ca.uhn.fhir.mdm.rules.matcher.models.MatchTypeEnum; 033import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 034import ca.uhn.fhir.util.FhirTerser; 035import org.apache.commons.lang3.Validate; 036import org.hl7.fhir.instance.model.api.IBase; 037import org.hl7.fhir.instance.model.api.IBaseResource; 038 039import java.util.List; 040import java.util.stream.Collectors; 041 042import static ca.uhn.fhir.mdm.api.MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE; 043 044/** 045 * This class is responsible for performing matching between raw-typed values of a left record and a right record. 046 */ 047public class MdmResourceFieldMatcher { 048 049 private final FhirContext myFhirContext; 050 private final MdmFieldMatchJson myMdmFieldMatchJson; 051 private final String myResourceType; 052 private final String myResourcePath; 053 private final String myFhirPath; 054 private final MdmRulesJson myMdmRulesJson; 055 private final String myName; 056 private final boolean myIsFhirPathExpression; 057 058 private final IMatcherFactory myIMatcherFactory; 059 060 public MdmResourceFieldMatcher( 061 FhirContext theFhirContext, 062 IMatcherFactory theIMatcherFactory, 063 MdmFieldMatchJson theMdmFieldMatchJson, 064 MdmRulesJson theMdmRulesJson) { 065 myIMatcherFactory = theIMatcherFactory; 066 067 myFhirContext = theFhirContext; 068 myMdmFieldMatchJson = theMdmFieldMatchJson; 069 myResourceType = theMdmFieldMatchJson.getResourceType(); 070 myResourcePath = theMdmFieldMatchJson.getResourcePath(); 071 myFhirPath = theMdmFieldMatchJson.getFhirPath(); 072 myName = theMdmFieldMatchJson.getName(); 073 myMdmRulesJson = theMdmRulesJson; 074 myIsFhirPathExpression = myFhirPath != null; 075 } 076 077 /** 078 * Compares two {@link IBaseResource}s and determines if they match, using the algorithm defined in this object's 079 * {@link MdmFieldMatchJson}. 080 * <p> 081 * In this implementation, it determines whether a given field matches between two resources. Internally this is evaluated using FhirPath. If any of the elements of theLeftResource 082 * match any of the elements of theRightResource, will return true. Otherwise, false. 083 * 084 * @param theLeftResource the first {@link IBaseResource} 085 * @param theRightResource the second {@link IBaseResource} 086 * @return A boolean indicating whether they match. 087 */ 088 public MdmMatchEvaluation match(IBaseResource theLeftResource, IBaseResource theRightResource) { 089 validate(theLeftResource); 090 validate(theRightResource); 091 092 List<IBase> leftValues; 093 List<IBase> rightValues; 094 095 if (myIsFhirPathExpression) { 096 IFhirPath fhirPath = myFhirContext.newFhirPath(); 097 leftValues = fhirPath.evaluate(theLeftResource, myFhirPath, IBase.class); 098 rightValues = fhirPath.evaluate(theRightResource, myFhirPath, IBase.class); 099 } else { 100 FhirTerser fhirTerser = myFhirContext.newTerser(); 101 leftValues = fhirTerser.getValues(theLeftResource, myResourcePath, IBase.class); 102 rightValues = fhirTerser.getValues(theRightResource, myResourcePath, IBase.class); 103 } 104 return match(leftValues, rightValues); 105 } 106 107 private MdmMatchEvaluation match(List<IBase> theLeftValues, List<IBase> theRightValues) { 108 MdmMatchEvaluation retval = new MdmMatchEvaluation(false, 0.0); 109 110 boolean isMatchingEmptyFieldValues = (theLeftValues.isEmpty() && theRightValues.isEmpty()); 111 IMdmFieldMatcher matcher = getFieldMatcher(); 112 if (isMatchingEmptyFieldValues && (matcher != null && matcher.isMatchingEmptyFields())) { 113 return match((IBase) null, (IBase) null); 114 } 115 116 for (IBase leftValue : theLeftValues) { 117 for (IBase rightValue : theRightValues) { 118 MdmMatchEvaluation nextMatch = match(leftValue, rightValue); 119 retval = MdmMatchEvaluation.max(retval, nextMatch); 120 } 121 } 122 123 return retval; 124 } 125 126 private MdmMatchEvaluation match(IBase theLeftValue, IBase theRightValue) { 127 IMdmFieldMatcher matcher = getFieldMatcher(); 128 if (matcher != null) { 129 boolean isMatches = matcher.matches(theLeftValue, theRightValue, myMdmFieldMatchJson.getMatcher()); 130 return new MdmMatchEvaluation(isMatches, isMatches ? 1.0 : 0.0); 131 } 132 133 MdmSimilarityJson similarity = myMdmFieldMatchJson.getSimilarity(); 134 if (similarity != null) { 135 return similarity.match(myFhirContext, theLeftValue, theRightValue); 136 } 137 138 throw new InternalErrorException( 139 Msg.code(1522) + "Field Match " + myName + " has neither a matcher nor a similarity."); 140 } 141 142 private void validate(IBaseResource theResource) { 143 String resourceType = myFhirContext.getResourceType(theResource); 144 Validate.notNull(resourceType, "Resource type may not be null"); 145 146 if (ALL_RESOURCE_SEARCH_PARAM_TYPE.equals(myResourceType)) { 147 boolean isMdmType = 148 myMdmRulesJson.getMdmTypes().stream().anyMatch(mdmType -> mdmType.equalsIgnoreCase(resourceType)); 149 Validate.isTrue( 150 isMdmType, 151 "Expecting resource type %s, got resource type %s", 152 myMdmRulesJson.getMdmTypes().stream().collect(Collectors.joining(",")), 153 resourceType); 154 } else { 155 Validate.isTrue( 156 myResourceType.equals(resourceType), 157 "Expecting resource type %s got resource type %s", 158 myResourceType, 159 resourceType); 160 } 161 } 162 163 public String getResourceType() { 164 return myResourceType; 165 } 166 167 public String getResourcePath() { 168 return myResourcePath; 169 } 170 171 public String getName() { 172 return myName; 173 } 174 175 private IMdmFieldMatcher getFieldMatcher() { 176 MdmMatcherJson matcherJson = myMdmFieldMatchJson.getMatcher(); 177 MatchTypeEnum matchTypeEnum = null; 178 if (matcherJson != null) { 179 matchTypeEnum = matcherJson.getAlgorithm(); 180 } 181 if (matchTypeEnum == null) { 182 return null; 183 } 184 185 return myIMatcherFactory.getFieldMatcherForMatchType(matchTypeEnum); 186 } 187}