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}