001/*-
002 * #%L
003 * HAPI FHIR - Master Data Management
004 * %%
005 * Copyright (C) 2014 - 2025 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.json;
021
022import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
023import ca.uhn.fhir.mdm.rules.matcher.util.MatchRuleUtil;
024import ca.uhn.fhir.model.api.IModelJson;
025import com.fasterxml.jackson.annotation.JsonProperty;
026import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
027import com.fasterxml.jackson.databind.util.StdConverter;
028import com.google.common.annotations.VisibleForTesting;
029import org.apache.commons.lang3.StringUtils;
030import org.apache.commons.lang3.Validate;
031
032import java.util.ArrayList;
033import java.util.Collections;
034import java.util.HashMap;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038import java.util.stream.Collectors;
039
040import static ca.uhn.fhir.mdm.api.MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE;
041
042@JsonDeserialize(converter = MdmRulesJson.MdmRulesJsonConverter.class)
043public class MdmRulesJson implements IModelJson {
044
045        @JsonProperty(value = "version", required = true)
046        String myVersion;
047
048        @JsonProperty(value = "candidateSearchParams", required = true)
049        List<MdmResourceSearchParamJson> myCandidateSearchParams = new ArrayList<>();
050
051        @JsonProperty(value = "candidateFilterSearchParams", required = true)
052        List<MdmFilterSearchParamJson> myCandidateFilterSearchParams = new ArrayList<>();
053
054        @JsonProperty(value = "matchFields", required = true)
055        List<MdmFieldMatchJson> myMatchFieldJsonList = new ArrayList<>();
056
057        @JsonProperty(value = "matchResultMap", required = true)
058        Map<String, MdmMatchResultEnum> myMatchResultMap = new HashMap<>();
059
060        /**
061         * This field is deprecated, use eidSystems instead.
062         */
063        @Deprecated
064        @JsonProperty(value = "eidSystem")
065        String myEnterpriseEIDSystem;
066
067        @JsonProperty(value = "eidSystems")
068        Map<String, String> myEnterpriseEidSystems = new HashMap<>();
069
070        @JsonProperty(value = "mdmTypes")
071        List<String> myMdmTypes;
072
073        transient VectorMatchResultMap myVectorMatchResultMap;
074
075        public void addMatchField(MdmFieldMatchJson theMatchRuleName) {
076                myMatchFieldJsonList.add(theMatchRuleName);
077        }
078
079        public void addResourceSearchParam(MdmResourceSearchParamJson theSearchParam) {
080                myCandidateSearchParams.add(theSearchParam);
081        }
082
083        public void addFilterSearchParam(MdmFilterSearchParamJson theSearchParam) {
084                myCandidateFilterSearchParams.add(theSearchParam);
085        }
086
087        int size() {
088                return myMatchFieldJsonList.size();
089        }
090
091        MdmFieldMatchJson get(int theIndex) {
092                return myMatchFieldJsonList.get(theIndex);
093        }
094
095        MdmMatchResultEnum getMatchResult(String theFieldMatchNames) {
096                return myMatchResultMap.get(theFieldMatchNames);
097        }
098
099        public MdmMatchResultEnum getMatchResult(Long theMatchVector) {
100                return myVectorMatchResultMap.get(theMatchVector);
101        }
102
103        public void putMatchResult(String theFieldMatchNames, MdmMatchResultEnum theMatchResult) {
104                myMatchResultMap.put(theFieldMatchNames, theMatchResult);
105                initialize();
106        }
107
108        Map<String, MdmMatchResultEnum> getMatchResultMap() {
109                return Collections.unmodifiableMap(myMatchResultMap);
110        }
111
112        /**
113         * Must call initialize() before calling getMatchResult(Long)
114         */
115        public void initialize() {
116                validate();
117                myVectorMatchResultMap = new VectorMatchResultMap(this);
118        }
119
120        public List<MdmFieldMatchJson> getMatchFields() {
121                return Collections.unmodifiableList(myMatchFieldJsonList);
122        }
123
124        public List<MdmResourceSearchParamJson> getCandidateSearchParams() {
125                return Collections.unmodifiableList(myCandidateSearchParams);
126        }
127
128        public List<MdmFilterSearchParamJson> getCandidateFilterSearchParams() {
129                return Collections.unmodifiableList(myCandidateFilterSearchParams);
130        }
131
132        /**
133         * Use {@link this#getEnterpriseEIDSystemForResourceType(String)} instead.
134         */
135        @Deprecated
136        public String getEnterpriseEIDSystem() {
137                return myEnterpriseEIDSystem;
138        }
139
140        /**
141         * Use {@link this#setEnterpriseEIDSystems(Map)} (String)} or {@link this#addEnterpriseEIDSystem(String, String)} instead.
142         */
143        @Deprecated
144        public void setEnterpriseEIDSystem(String theEnterpriseEIDSystem) {
145                myEnterpriseEIDSystem = theEnterpriseEIDSystem;
146        }
147
148        public void setEnterpriseEIDSystems(Map<String, String> theEnterpriseEIDSystems) {
149                myEnterpriseEidSystems = theEnterpriseEIDSystems;
150        }
151
152        public void addEnterpriseEIDSystem(String theResourceType, String theEidSystem) {
153                if (myEnterpriseEidSystems == null) {
154                        myEnterpriseEidSystems = new HashMap<>();
155                }
156                myEnterpriseEidSystems.put(theResourceType, theEidSystem);
157        }
158
159        public Map<String, String> getEnterpriseEIDSystems() {
160                // First try the new property.
161                if (myEnterpriseEidSystems != null && !myEnterpriseEidSystems.isEmpty()) {
162                        return myEnterpriseEidSystems;
163                        // If that fails, fall back to our deprecated property.
164                } else if (!StringUtils.isBlank(myEnterpriseEIDSystem)) {
165                        HashMap<String, String> retVal = new HashMap<>();
166                        retVal.put(ALL_RESOURCE_SEARCH_PARAM_TYPE, myEnterpriseEIDSystem);
167                        return retVal;
168                        // Otherwise, return an empty map.
169                } else {
170                        return Collections.emptyMap();
171                }
172        }
173
174        public String getEnterpriseEIDSystemForResourceType(String theResourceType) {
175                Map<String, String> enterpriseEIDSystems = getEnterpriseEIDSystems();
176                if (enterpriseEIDSystems.containsKey(ALL_RESOURCE_SEARCH_PARAM_TYPE)) {
177                        return enterpriseEIDSystems.get(ALL_RESOURCE_SEARCH_PARAM_TYPE);
178                } else {
179                        return enterpriseEIDSystems.get(theResourceType);
180                }
181        }
182
183        public String getVersion() {
184                return myVersion;
185        }
186
187        public MdmRulesJson setVersion(String theVersion) {
188                myVersion = theVersion;
189                return this;
190        }
191
192        private void validate() {
193                Validate.notBlank(myVersion, "version may not be blank");
194
195                Map<String, String> enterpriseEIDSystems = getEnterpriseEIDSystems();
196
197                // If we have a * eid system, there should only be one.
198                if (enterpriseEIDSystems.containsKey(ALL_RESOURCE_SEARCH_PARAM_TYPE)) {
199                        Validate.isTrue(enterpriseEIDSystems.size() == 1);
200                }
201
202                Validate.isTrue(
203                                MatchRuleUtil.canHandleRuleCount(myMatchFieldJsonList),
204                                String.format(
205                                                "MDM cannot guarantee accuracy with more than %d match fields.", MatchRuleUtil.MAX_RULE_COUNT));
206        }
207
208        public String getSummary() {
209                return myCandidateSearchParams.size() + " Candidate Search Params, " + myCandidateFilterSearchParams.size()
210                                + " Filter Search Params, " + myMatchFieldJsonList.size()
211                                + " Match Fields, " + myMatchResultMap.size()
212                                + " Match Result Entries";
213        }
214
215        public String getFieldMatchNamesForVector(long theVector) {
216                return myVectorMatchResultMap.getFieldMatchNames(theVector);
217        }
218
219        public Set<Map.Entry<String, MdmMatchResultEnum>> getMatchedRulesFromVectorMap(Long theLong) {
220                Set<String> matchedRules = myVectorMatchResultMap.getMatchedRules(theLong);
221                return myMatchResultMap.entrySet().stream()
222                                .filter(e -> matchedRules.contains(e.getKey()))
223                                .collect(Collectors.toSet());
224        }
225
226        public String getDetailedFieldMatchResultWithSuccessInformation(long theVector) {
227                List<String> fieldMatchResult = new ArrayList<>();
228                for (int i = 0; i < myMatchFieldJsonList.size(); ++i) {
229                        if ((theVector & (1 << i)) == 0) {
230                                fieldMatchResult.add(myMatchFieldJsonList.get(i).getName() + ": NO");
231                        } else {
232                                fieldMatchResult.add(myMatchFieldJsonList.get(i).getName() + ": YES");
233                        }
234                }
235                return String.join("\n", fieldMatchResult);
236        }
237
238        @VisibleForTesting
239        VectorMatchResultMap getVectorMatchResultMapForUnitTest() {
240                return myVectorMatchResultMap;
241        }
242
243        /**
244         * Ensure the vector map is initialized after we deserialize
245         */
246        static class MdmRulesJsonConverter extends StdConverter<MdmRulesJson, MdmRulesJson> {
247
248                /**
249                 * This empty constructor is required by Jackson
250                 */
251                public MdmRulesJsonConverter() {}
252
253                @Override
254                public MdmRulesJson convert(MdmRulesJson theMdmRulesJson) {
255                        theMdmRulesJson.initialize();
256                        return theMdmRulesJson;
257                }
258        }
259
260        public List<String> getMdmTypes() {
261                return myMdmTypes;
262        }
263
264        public void setMdmTypes(List<String> theMdmTypes) {
265                myMdmTypes = theMdmTypes;
266        }
267}