
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}