001package org.hl7.fhir.common.hapi.validation.support; 002 003import ca.uhn.fhir.context.ConfigurationException; 004import ca.uhn.fhir.context.FhirContext; 005import ca.uhn.fhir.context.FhirVersionEnum; 006import ca.uhn.fhir.context.support.ConceptValidationOptions; 007import ca.uhn.fhir.context.support.IValidationSupport; 008import ca.uhn.fhir.context.support.LookupCodeRequest; 009import ca.uhn.fhir.context.support.TranslateConceptResults; 010import ca.uhn.fhir.context.support.ValidationSupportContext; 011import ca.uhn.fhir.context.support.ValueSetExpansionOptions; 012import ca.uhn.fhir.i18n.Msg; 013import ca.uhn.fhir.util.Logs; 014import jakarta.annotation.Nonnull; 015import org.apache.commons.lang3.Validate; 016import org.hl7.fhir.instance.model.api.IBaseResource; 017import org.hl7.fhir.instance.model.api.IPrimitiveType; 018import org.slf4j.Logger; 019 020import java.util.ArrayList; 021import java.util.HashSet; 022import java.util.List; 023import java.util.Optional; 024import java.util.Set; 025import java.util.function.Function; 026 027import static org.apache.commons.lang3.StringUtils.isBlank; 028import static org.apache.commons.lang3.StringUtils.isNotBlank; 029 030public class ValidationSupportChain implements IValidationSupport { 031 static Logger ourLog = Logs.getTerminologyTroubleshootingLog(); 032 033 private List<IValidationSupport> myChain; 034 035 /** 036 * Constructor 037 */ 038 public ValidationSupportChain() { 039 myChain = new ArrayList<>(); 040 } 041 042 /** 043 * Constructor 044 */ 045 public ValidationSupportChain(IValidationSupport... theValidationSupportModules) { 046 this(); 047 for (IValidationSupport next : theValidationSupportModules) { 048 if (next != null) { 049 addValidationSupport(next); 050 } 051 } 052 } 053 054 @Override 055 public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) { 056 TranslateConceptResults retVal = null; 057 for (IValidationSupport next : myChain) { 058 TranslateConceptResults translations = next.translateConcept(theRequest); 059 if (translations != null) { 060 if (retVal == null) { 061 retVal = new TranslateConceptResults(); 062 } 063 064 if (retVal.getMessage() == null) { 065 retVal.setMessage(translations.getMessage()); 066 } 067 068 if (translations.getResult() && !retVal.getResult()) { 069 retVal.setResult(translations.getResult()); 070 retVal.setMessage(translations.getMessage()); 071 } 072 073 if (!translations.isEmpty()) { 074 if (ourLog.isDebugEnabled()) { 075 ourLog.debug( 076 "{} found {} concept translation{} for {}", 077 next.getName(), 078 translations.size(), 079 translations.size() > 1 ? "s" : "", 080 theRequest); 081 } 082 retVal.getResults().addAll(translations.getResults()); 083 } 084 } 085 } 086 return retVal; 087 } 088 089 @Override 090 public void invalidateCaches() { 091 ourLog.debug("Invalidating caches in {} validation support modules", myChain.size()); 092 for (IValidationSupport next : myChain) { 093 next.invalidateCaches(); 094 } 095 } 096 097 @Override 098 public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { 099 for (IValidationSupport next : myChain) { 100 boolean retVal = next.isValueSetSupported(theValidationSupportContext, theValueSetUrl); 101 if (retVal) { 102 if (ourLog.isDebugEnabled()) { 103 ourLog.debug("ValueSet {} found in {}", theValueSetUrl, next.getName()); 104 } 105 return true; 106 } 107 } 108 return false; 109 } 110 111 @Override 112 public IBaseResource generateSnapshot( 113 ValidationSupportContext theValidationSupportContext, 114 IBaseResource theInput, 115 String theUrl, 116 String theWebUrl, 117 String theProfileName) { 118 for (IValidationSupport next : myChain) { 119 IBaseResource retVal = 120 next.generateSnapshot(theValidationSupportContext, theInput, theUrl, theWebUrl, theProfileName); 121 if (retVal != null) { 122 if (ourLog.isDebugEnabled()) { 123 ourLog.debug("Profile snapshot for {} generated by {}", theInput.getIdElement(), next.getName()); 124 } 125 return retVal; 126 } 127 } 128 return null; 129 } 130 131 @Override 132 public FhirContext getFhirContext() { 133 if (myChain.size() == 0) { 134 return null; 135 } 136 return myChain.get(0).getFhirContext(); 137 } 138 139 /** 140 * Add a validation support module to the chain. 141 * <p> 142 * Note that this method is not thread-safe. All validation support modules should be added prior to use. 143 * </p> 144 * 145 * @param theValidationSupport The validation support. Must not be null, and must have a {@link #getFhirContext() FhirContext} that is configured for the same FHIR version as other entries in the chain. 146 */ 147 public void addValidationSupport(IValidationSupport theValidationSupport) { 148 int index = myChain.size(); 149 addValidationSupport(index, theValidationSupport); 150 } 151 152 /** 153 * Add a validation support module to the chain at the given index. 154 * <p> 155 * Note that this method is not thread-safe. All validation support modules should be added prior to use. 156 * </p> 157 * 158 * @param theIndex The index to add to 159 * @param theValidationSupport The validation support. Must not be null, and must have a {@link #getFhirContext() FhirContext} that is configured for the same FHIR version as other entries in the chain. 160 */ 161 public void addValidationSupport(int theIndex, IValidationSupport theValidationSupport) { 162 Validate.notNull(theValidationSupport, "theValidationSupport must not be null"); 163 164 if (theValidationSupport.getFhirContext() == null) { 165 String message = "Can not add validation support: getFhirContext() returns null"; 166 throw new ConfigurationException(Msg.code(708) + message); 167 } 168 169 FhirContext existingFhirContext = getFhirContext(); 170 if (existingFhirContext != null) { 171 FhirVersionEnum newVersion = 172 theValidationSupport.getFhirContext().getVersion().getVersion(); 173 FhirVersionEnum existingVersion = existingFhirContext.getVersion().getVersion(); 174 if (!existingVersion.equals(newVersion)) { 175 String message = "Trying to add validation support of version " + newVersion + " to chain with " 176 + myChain.size() + " entries of version " + existingVersion; 177 throw new ConfigurationException(Msg.code(709) + message); 178 } 179 } 180 181 myChain.add(theIndex, theValidationSupport); 182 } 183 184 /** 185 * Removes an item from the chain. Note that this method is mostly intended for testing. Removing items from the chain while validation is 186 * actually occurring is not an expected use case for this class. 187 */ 188 public void removeValidationSupport(IValidationSupport theValidationSupport) { 189 myChain.remove(theValidationSupport); 190 } 191 192 @Override 193 public ValueSetExpansionOutcome expandValueSet( 194 ValidationSupportContext theValidationSupportContext, 195 ValueSetExpansionOptions theExpansionOptions, 196 @Nonnull IBaseResource theValueSetToExpand) { 197 for (IValidationSupport next : myChain) { 198 // TODO: test if code system is supported? 199 ValueSetExpansionOutcome expanded = 200 next.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand); 201 if (expanded != null) { 202 if (ourLog.isDebugEnabled()) { 203 ourLog.debug("ValueSet {} expanded by {}", theValueSetToExpand.getIdElement(), next.getName()); 204 } 205 return expanded; 206 } 207 } 208 return null; 209 } 210 211 @Override 212 public boolean isRemoteTerminologyServiceConfigured() { 213 if (myChain != null) { 214 Optional<IValidationSupport> remoteTerminologyService = myChain.stream() 215 .filter(RemoteTerminologyServiceValidationSupport.class::isInstance) 216 .findFirst(); 217 if (remoteTerminologyService.isPresent()) { 218 return true; 219 } 220 } 221 return false; 222 } 223 224 @Override 225 public List<IBaseResource> fetchAllConformanceResources() { 226 List<IBaseResource> retVal = new ArrayList<>(); 227 for (IValidationSupport next : myChain) { 228 List<IBaseResource> candidates = next.fetchAllConformanceResources(); 229 if (candidates != null) { 230 retVal.addAll(candidates); 231 } 232 } 233 return retVal; 234 } 235 236 @Override 237 public List<IBaseResource> fetchAllStructureDefinitions() { 238 return doFetchStructureDefinitions(t -> t.fetchAllStructureDefinitions()); 239 } 240 241 @Override 242 public List<IBaseResource> fetchAllNonBaseStructureDefinitions() { 243 return doFetchStructureDefinitions(t -> t.fetchAllNonBaseStructureDefinitions()); 244 } 245 246 private List<IBaseResource> doFetchStructureDefinitions( 247 Function<IValidationSupport, List<IBaseResource>> theFunction) { 248 ArrayList<IBaseResource> retVal = new ArrayList<>(); 249 Set<String> urls = new HashSet<>(); 250 for (IValidationSupport nextSupport : myChain) { 251 List<IBaseResource> allStructureDefinitions = theFunction.apply(nextSupport); 252 if (allStructureDefinitions != null) { 253 for (IBaseResource next : allStructureDefinitions) { 254 255 IPrimitiveType<?> urlType = 256 getFhirContext().newTerser().getSingleValueOrNull(next, "url", IPrimitiveType.class); 257 if (urlType == null 258 || isBlank(urlType.getValueAsString()) 259 || urls.add(urlType.getValueAsString())) { 260 retVal.add(next); 261 } 262 } 263 } 264 } 265 return retVal; 266 } 267 268 @Override 269 public IBaseResource fetchCodeSystem(String theSystem) { 270 for (IValidationSupport next : myChain) { 271 IBaseResource retVal = next.fetchCodeSystem(theSystem); 272 if (retVal != null) { 273 if (ourLog.isDebugEnabled()) { 274 ourLog.debug( 275 "CodeSystem {} with System {} fetched by {}", 276 retVal.getIdElement(), 277 theSystem, 278 next.getName()); 279 } 280 return retVal; 281 } 282 } 283 return null; 284 } 285 286 @Override 287 public IBaseResource fetchValueSet(String theUrl) { 288 for (IValidationSupport next : myChain) { 289 IBaseResource retVal = next.fetchValueSet(theUrl); 290 if (retVal != null) { 291 if (ourLog.isDebugEnabled()) { 292 ourLog.debug( 293 "ValueSet {} with URL {} fetched by {}", retVal.getIdElement(), theUrl, next.getName()); 294 } 295 return retVal; 296 } 297 } 298 return null; 299 } 300 301 @Override 302 public <T extends IBaseResource> T fetchResource(Class<T> theClass, String theUri) { 303 for (IValidationSupport next : myChain) { 304 T retVal = next.fetchResource(theClass, theUri); 305 if (retVal != null) { 306 if (ourLog.isDebugEnabled()) { 307 ourLog.debug( 308 "Resource {} with URI {} fetched by {}", retVal.getIdElement(), theUri, next.getName()); 309 } 310 return retVal; 311 } 312 } 313 return null; 314 } 315 316 @Override 317 public byte[] fetchBinary(String key) { 318 for (IValidationSupport next : myChain) { 319 byte[] retVal = next.fetchBinary(key); 320 if (retVal != null) { 321 if (ourLog.isDebugEnabled()) { 322 ourLog.debug("Binary with key {} fetched by {}", key, next.getName()); 323 } 324 return retVal; 325 } 326 } 327 return null; 328 } 329 330 @Override 331 public IBaseResource fetchStructureDefinition(String theUrl) { 332 for (IValidationSupport next : myChain) { 333 IBaseResource retVal = next.fetchStructureDefinition(theUrl); 334 if (retVal != null) { 335 if (ourLog.isDebugEnabled()) { 336 ourLog.debug("StructureDefinition with URL {} fetched by {}", theUrl, next.getName()); 337 } 338 return retVal; 339 } 340 } 341 return null; 342 } 343 344 @Override 345 public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { 346 for (IValidationSupport next : myChain) { 347 if (next.isCodeSystemSupported(theValidationSupportContext, theSystem)) { 348 if (ourLog.isDebugEnabled()) { 349 ourLog.debug("CodeSystem with System {} is supported by {}", theSystem, next.getName()); 350 } 351 return true; 352 } 353 } 354 return false; 355 } 356 357 @Override 358 public CodeValidationResult validateCode( 359 @Nonnull ValidationSupportContext theValidationSupportContext, 360 @Nonnull ConceptValidationOptions theOptions, 361 String theCodeSystem, 362 String theCode, 363 String theDisplay, 364 String theValueSetUrl) { 365 for (IValidationSupport next : myChain) { 366 if ((isBlank(theValueSetUrl) && next.isCodeSystemSupported(theValidationSupportContext, theCodeSystem)) 367 || (isNotBlank(theValueSetUrl) 368 && next.isValueSetSupported(theValidationSupportContext, theValueSetUrl))) { 369 CodeValidationResult retVal = next.validateCode( 370 theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl); 371 if (retVal != null) { 372 if (ourLog.isDebugEnabled()) { 373 ourLog.debug( 374 "Code {}|{} '{}' in ValueSet {} validated by {}", 375 theCodeSystem, 376 theCode, 377 theDisplay, 378 theValueSetUrl, 379 next.getName()); 380 } 381 return retVal; 382 } 383 } 384 } 385 return null; 386 } 387 388 @Override 389 public CodeValidationResult validateCodeInValueSet( 390 ValidationSupportContext theValidationSupportContext, 391 ConceptValidationOptions theOptions, 392 String theCodeSystem, 393 String theCode, 394 String theDisplay, 395 @Nonnull IBaseResource theValueSet) { 396 for (IValidationSupport next : myChain) { 397 String url = CommonCodeSystemsTerminologyService.getValueSetUrl(getFhirContext(), theValueSet); 398 if (isBlank(url) || next.isValueSetSupported(theValidationSupportContext, url)) { 399 CodeValidationResult retVal = next.validateCodeInValueSet( 400 theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSet); 401 if (retVal != null) { 402 if (ourLog.isDebugEnabled()) { 403 ourLog.debug( 404 "Code {}|{} '{}' in ValueSet {} validated by {}", 405 theCodeSystem, 406 theCode, 407 theDisplay, 408 theValueSet.getIdElement(), 409 next.getName()); 410 } 411 return retVal; 412 } 413 } 414 } 415 return null; 416 } 417 418 @Override 419 public LookupCodeResult lookupCode( 420 ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) { 421 for (IValidationSupport next : myChain) { 422 final String system = theLookupCodeRequest.getSystem(); 423 final String code = theLookupCodeRequest.getCode(); 424 final String displayLanguage = theLookupCodeRequest.getDisplayLanguage(); 425 if (next.isCodeSystemSupported(theValidationSupportContext, system)) { 426 LookupCodeResult lookupCodeResult = next.lookupCode(theValidationSupportContext, theLookupCodeRequest); 427 if (lookupCodeResult == null) { 428 /* 429 This branch has been added as a fall-back mechanism for supporting lookupCode 430 methods marked as deprecated in interface IValidationSupport. 431 */ 432 lookupCodeResult = next.lookupCode(theValidationSupportContext, system, code, displayLanguage); 433 } 434 if (ourLog.isDebugEnabled()) { 435 ourLog.debug( 436 "Code {}|{}{} {} by {}", 437 system, 438 code, 439 isBlank(displayLanguage) ? "" : " (" + theLookupCodeRequest.getDisplayLanguage() + ")", 440 lookupCodeResult != null && lookupCodeResult.isFound() ? "found" : "not found", 441 next.getName()); 442 } 443 return lookupCodeResult; 444 } 445 } 446 return null; 447 } 448}