001package ca.uhn.fhir.mdm.rules.svc;
002
003/*-
004 * #%L
005 * HAPI FHIR - Master Data Management
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.fhirpath.IFhirPath;
025import ca.uhn.fhir.mdm.api.MdmMatchEvaluation;
026import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson;
027import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
028import ca.uhn.fhir.util.FhirTerser;
029import org.apache.commons.lang3.Validate;
030import org.hl7.fhir.instance.model.api.IBase;
031import org.hl7.fhir.instance.model.api.IBaseResource;
032
033import java.util.List;
034import java.util.stream.Collectors;
035
036import static ca.uhn.fhir.mdm.api.MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE;
037
038/**
039 * This class is responsible for performing matching between raw-typed values of a left record and a right record.
040 */
041public class MdmResourceFieldMatcher {
042
043        private final FhirContext myFhirContext;
044        private final MdmFieldMatchJson myMdmFieldMatchJson;
045        private final String myResourceType;
046        private final String myResourcePath;
047        private final String myFhirPath;
048        private final MdmRulesJson myMdmRulesJson;
049        private final String myName;
050        private final boolean myIsFhirPathExpression;
051
052        public MdmResourceFieldMatcher(FhirContext theFhirContext, MdmFieldMatchJson theMdmFieldMatchJson, MdmRulesJson theMdmRulesJson) {
053                myFhirContext = theFhirContext;
054                myMdmFieldMatchJson = theMdmFieldMatchJson;
055                myResourceType = theMdmFieldMatchJson.getResourceType();
056                myResourcePath = theMdmFieldMatchJson.getResourcePath();
057                myFhirPath = theMdmFieldMatchJson.getFhirPath();
058                myName = theMdmFieldMatchJson.getName();
059                myMdmRulesJson = theMdmRulesJson;
060                myIsFhirPathExpression = myFhirPath != null;
061        }
062
063        /**
064         * Compares two {@link IBaseResource}s and determines if they match, using the algorithm defined in this object's
065         * {@link MdmFieldMatchJson}.
066         * <p>
067         * 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
068         * match any of the elements of theRightResource, will return true. Otherwise, false.
069         *
070         * @param theLeftResource  the first {@link IBaseResource}
071         * @param theRightResource the second {@link IBaseResource}
072         * @return A boolean indicating whether they match.
073         */
074        @SuppressWarnings("rawtypes")
075        public MdmMatchEvaluation match(IBaseResource theLeftResource, IBaseResource theRightResource) {
076                validate(theLeftResource);
077                validate(theRightResource);
078
079                List<IBase> leftValues;
080                List<IBase> rightValues;
081
082                if (myIsFhirPathExpression) {
083                        IFhirPath fhirPath = myFhirContext.newFhirPath();
084                        leftValues = fhirPath.evaluate(theLeftResource, myFhirPath, IBase.class);
085                        rightValues = fhirPath.evaluate(theRightResource, myFhirPath, IBase.class);
086                } else {
087                        FhirTerser fhirTerser = myFhirContext.newTerser();
088                        leftValues = fhirTerser.getValues(theLeftResource, myResourcePath, IBase.class);
089                        rightValues = fhirTerser.getValues(theRightResource, myResourcePath, IBase.class);
090                }
091                return match(leftValues, rightValues);
092        }
093
094        @SuppressWarnings("rawtypes")
095        private MdmMatchEvaluation match(List<IBase> theLeftValues, List<IBase> theRightValues) {
096                MdmMatchEvaluation retval = new MdmMatchEvaluation(false, 0.0);
097
098                boolean isMatchingEmptyFieldValues = (theLeftValues.isEmpty() && theRightValues.isEmpty());
099                if (isMatchingEmptyFieldValues && myMdmFieldMatchJson.isMatcherSupportingEmptyFields()) {
100                        return match((IBase) null, (IBase) null);
101                }
102
103                for (IBase leftValue : theLeftValues) {
104                        for (IBase rightValue : theRightValues) {
105                                MdmMatchEvaluation nextMatch = match(leftValue, rightValue);
106                                retval = MdmMatchEvaluation.max(retval, nextMatch);
107                        }
108                }
109
110                return retval;
111        }
112
113        private MdmMatchEvaluation match(IBase theLeftValue, IBase theRightValue) {
114                return myMdmFieldMatchJson.match(myFhirContext, theLeftValue, theRightValue);
115        }
116
117        private void validate(IBaseResource theResource) {
118                String resourceType = myFhirContext.getResourceType(theResource);
119                Validate.notNull(resourceType, "Resource type may not be null");
120
121                if (ALL_RESOURCE_SEARCH_PARAM_TYPE.equals(myResourceType)) {
122                        boolean isMdmType = myMdmRulesJson.getMdmTypes().stream().anyMatch(mdmType -> mdmType.equalsIgnoreCase(resourceType));
123                        Validate.isTrue(isMdmType, "Expecting resource type %s, got resource type %s", myMdmRulesJson.getMdmTypes().stream().collect(Collectors.joining(",")), resourceType);
124                } else {
125                        Validate.isTrue(myResourceType.equals(resourceType), "Expecting resource type %s got resource type %s", myResourceType, resourceType);
126                }
127        }
128
129        public String getResourceType() {
130                return myResourceType;
131        }
132
133        public String getResourcePath() {
134                return myResourcePath;
135        }
136
137        public String getName() {
138                return myName;
139        }
140}