001/* 002 * #%L 003 * HAPI FHIR - Core Library 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.validation; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.HookParams; 025import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 026import ca.uhn.fhir.interceptor.api.Pointcut; 027import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 028import ca.uhn.fhir.util.BundleUtil; 029import ca.uhn.fhir.util.TerserUtil; 030import ca.uhn.fhir.validation.schematron.SchematronProvider; 031import org.apache.commons.lang3.StringUtils; 032import org.apache.commons.lang3.Validate; 033import org.hl7.fhir.instance.model.api.IBaseBundle; 034import org.hl7.fhir.instance.model.api.IBaseResource; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037 038import java.util.ArrayList; 039import java.util.List; 040import java.util.concurrent.ExecutionException; 041import java.util.concurrent.ExecutorService; 042import java.util.concurrent.Future; 043import java.util.function.Function; 044import java.util.stream.Collectors; 045import java.util.stream.IntStream; 046 047import static org.apache.commons.lang3.StringUtils.isBlank; 048 049/** 050 * Resource validator, which checks resources for compliance against various validation schemes (schemas, schematrons, profiles, etc.) 051 * 052 * <p> 053 * To obtain a resource validator, call {@link FhirContext#newValidator()} 054 * </p> 055 * 056 * <p> 057 * <b>Thread safety note:</b> This class is thread safe, so you may register or unregister validator modules at any time. Individual modules are not guaranteed to be thread safe however. Reconfigure 058 * them with caution. 059 * </p> 060 */ 061public class FhirValidator { 062 private static final Logger ourLog = LoggerFactory.getLogger(FhirValidator.class); 063 064 private static final String I18N_KEY_NO_PH_ERROR = FhirValidator.class.getName() + ".noPhError"; 065 066 private static volatile Boolean ourPhPresentOnClasspath; 067 private final FhirContext myContext; 068 private List<IValidatorModule> myValidators = new ArrayList<>(); 069 private IInterceptorBroadcaster myInterceptorBroadcaster; 070 private boolean myConcurrentBundleValidation; 071 private boolean mySkipContainedReferenceValidation; 072 073 private ExecutorService myExecutorService; 074 075 /** 076 * Constructor (this should not be called directly, but rather {@link FhirContext#newValidator()} should be called to obtain an instance of {@link FhirValidator}) 077 */ 078 public FhirValidator(FhirContext theFhirContext) { 079 myContext = theFhirContext; 080 081 if (ourPhPresentOnClasspath == null) { 082 ourPhPresentOnClasspath = SchematronProvider.isSchematronAvailable(theFhirContext); 083 } 084 } 085 086 private void addOrRemoveValidator( 087 boolean theValidateAgainstStandardSchema, 088 Class<? extends IValidatorModule> type, 089 IValidatorModule theInstance) { 090 if (theValidateAgainstStandardSchema) { 091 boolean found = haveValidatorOfType(type); 092 if (!found) { 093 registerValidatorModule(theInstance); 094 } 095 } else { 096 for (IValidatorModule next : myValidators) { 097 if (next.getClass().equals(type)) { 098 unregisterValidatorModule(next); 099 } 100 } 101 } 102 } 103 104 private boolean haveValidatorOfType(Class<? extends IValidatorModule> type) { 105 boolean found = false; 106 for (IValidatorModule next : myValidators) { 107 if (next.getClass().equals(type)) { 108 found = true; 109 break; 110 } 111 } 112 return found; 113 } 114 115 /** 116 * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself) 117 */ 118 public synchronized boolean isValidateAgainstStandardSchema() { 119 return haveValidatorOfType(SchemaBaseValidator.class); 120 } 121 122 /** 123 * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself) 124 * 125 * @return Returns a referens to <code>this<code> for method chaining 126 */ 127 public synchronized FhirValidator setValidateAgainstStandardSchema(boolean theValidateAgainstStandardSchema) { 128 addOrRemoveValidator( 129 theValidateAgainstStandardSchema, SchemaBaseValidator.class, new SchemaBaseValidator(myContext)); 130 return this; 131 } 132 133 /** 134 * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself) 135 */ 136 public synchronized boolean isValidateAgainstStandardSchematron() { 137 if (!ourPhPresentOnClasspath) { 138 // No need to ask since we dont have Ph-Schematron. Also Class.forname will complain 139 // about missing ph-schematron import. 140 return false; 141 } 142 Class<? extends IValidatorModule> cls = SchematronProvider.getSchematronValidatorClass(); 143 return haveValidatorOfType(cls); 144 } 145 146 /** 147 * Should the validator validate the resource against the base schematron (the schematron provided with the FHIR distribution itself) 148 * 149 * @return Returns a referens to <code>this<code> for method chaining 150 */ 151 public synchronized FhirValidator setValidateAgainstStandardSchematron( 152 boolean theValidateAgainstStandardSchematron) { 153 if (theValidateAgainstStandardSchematron && !ourPhPresentOnClasspath) { 154 throw new IllegalArgumentException( 155 Msg.code(1970) + myContext.getLocalizer().getMessage(I18N_KEY_NO_PH_ERROR)); 156 } 157 if (!theValidateAgainstStandardSchematron && !ourPhPresentOnClasspath) { 158 return this; 159 } 160 Class<? extends IValidatorModule> cls = SchematronProvider.getSchematronValidatorClass(); 161 IValidatorModule instance = SchematronProvider.getSchematronValidatorInstance(myContext); 162 addOrRemoveValidator(theValidateAgainstStandardSchematron, cls, instance); 163 return this; 164 } 165 166 /** 167 * Add a new validator module to this validator. You may register as many modules as you like at any time. 168 * 169 * @param theValidator The validator module. Must not be null. 170 * @return Returns a reference to <code>this</code> for easy method chaining. 171 */ 172 public synchronized FhirValidator registerValidatorModule(IValidatorModule theValidator) { 173 Validate.notNull(theValidator, "theValidator must not be null"); 174 ArrayList<IValidatorModule> newValidators = new ArrayList<>(myValidators.size() + 1); 175 newValidators.addAll(myValidators); 176 newValidators.add(theValidator); 177 178 myValidators = newValidators; 179 return this; 180 } 181 182 /** 183 * Removes a validator module from this validator. You may register as many modules as you like, and remove them at any time. 184 * 185 * @param theValidator The validator module. Must not be null. 186 */ 187 public synchronized void unregisterValidatorModule(IValidatorModule theValidator) { 188 Validate.notNull(theValidator, "theValidator must not be null"); 189 ArrayList<IValidatorModule> newValidators = new ArrayList<IValidatorModule>(myValidators.size() + 1); 190 newValidators.addAll(myValidators); 191 newValidators.remove(theValidator); 192 193 myValidators = newValidators; 194 } 195 196 private void applyDefaultValidators() { 197 if (myValidators.isEmpty()) { 198 setValidateAgainstStandardSchema(true); 199 if (ourPhPresentOnClasspath) { 200 setValidateAgainstStandardSchematron(true); 201 } 202 } 203 } 204 205 /** 206 * Validates a resource instance returning a {@link ValidationResult} which contains the results. 207 * 208 * @param theResource the resource to validate 209 * @return the results of validation 210 * @since 0.7 211 */ 212 public ValidationResult validateWithResult(IBaseResource theResource) { 213 return validateWithResult(theResource, null); 214 } 215 216 /** 217 * Validates a resource instance returning a {@link ValidationResult} which contains the results. 218 * 219 * @param theResource the resource to validate 220 * @return the results of validation 221 * @since 1.1 222 */ 223 public ValidationResult validateWithResult(String theResource) { 224 return validateWithResult(theResource, null); 225 } 226 227 /** 228 * Validates a resource instance returning a {@link ValidationResult} which contains the results. 229 * 230 * @param theResource the resource to validate 231 * @param theOptions Optionally provides options to the validator 232 * @return the results of validation 233 * @since 4.0.0 234 */ 235 public ValidationResult validateWithResult(String theResource, ValidationOptions theOptions) { 236 Validate.notNull(theResource, "theResource must not be null"); 237 IValidationContext<IBaseResource> validationContext = 238 ValidationContext.forText(myContext, theResource, theOptions); 239 Function<ValidationResult, ValidationResult> callback = 240 result -> invokeValidationCompletedHooks(null, theResource, result); 241 return doValidate(validationContext, theOptions, callback); 242 } 243 244 /** 245 * Validates a resource instance returning a {@link ValidationResult} which contains the results. 246 * 247 * @param theResource the resource to validate 248 * @param theOptions Optionally provides options to the validator 249 * @return the results of validation 250 * @since 4.0.0 251 */ 252 public ValidationResult validateWithResult(IBaseResource theResource, ValidationOptions theOptions) { 253 Validate.notNull(theResource, "theResource must not be null"); 254 IValidationContext<IBaseResource> validationContext = 255 ValidationContext.forResource(myContext, theResource, theOptions); 256 Function<ValidationResult, ValidationResult> callback = 257 result -> invokeValidationCompletedHooks(theResource, null, result); 258 return doValidate(validationContext, theOptions, callback); 259 } 260 261 private ValidationResult doValidate( 262 IValidationContext<IBaseResource> theValidationContext, 263 ValidationOptions theOptions, 264 Function<ValidationResult, ValidationResult> theValidationCompletionCallback) { 265 applyDefaultValidators(); 266 267 ValidationResult result; 268 if (myConcurrentBundleValidation 269 && theValidationContext.getResource() instanceof IBaseBundle 270 && myExecutorService != null) { 271 result = validateBundleEntriesConcurrently(theValidationContext, theOptions); 272 } else { 273 result = validateResource(theValidationContext); 274 } 275 276 return theValidationCompletionCallback.apply(result); 277 } 278 279 private ValidationResult validateBundleEntriesConcurrently( 280 IValidationContext<IBaseResource> theValidationContext, ValidationOptions theOptions) { 281 List<IBaseResource> entries = 282 BundleUtil.toListOfResources(myContext, (IBaseBundle) theValidationContext.getResource()); 283 // Async validation tasks 284 List<ConcurrentValidationTask> validationTasks = IntStream.range(0, entries.size()) 285 .mapToObj(index -> { 286 IBaseResource resourceToValidate; 287 IBaseResource entry = entries.get(index); 288 289 if (mySkipContainedReferenceValidation) { 290 resourceToValidate = withoutContainedResources(entry); 291 } else { 292 resourceToValidate = entry; 293 } 294 295 String entryPathPrefix = 296 String.format("Bundle.entry[%d].resource.ofType(%s)", index, resourceToValidate.fhirType()); 297 Future<ValidationResult> future = myExecutorService.submit(() -> { 298 IValidationContext<IBaseResource> entryValidationContext = ValidationContext.forResource( 299 theValidationContext.getFhirContext(), resourceToValidate, theOptions); 300 return validateResource(entryValidationContext); 301 }); 302 return new ConcurrentValidationTask(entryPathPrefix, future); 303 }) 304 .collect(Collectors.toList()); 305 306 List<SingleValidationMessage> validationMessages = buildValidationMessages(validationTasks); 307 return new ValidationResult(myContext, validationMessages); 308 } 309 310 IBaseResource withoutContainedResources(IBaseResource theEntry) { 311 if (TerserUtil.hasValues(myContext, theEntry, "contained")) { 312 IBaseResource deepCopy = TerserUtil.clone(myContext, theEntry); 313 TerserUtil.clearField(myContext, deepCopy, "contained"); 314 return deepCopy; 315 } else { 316 return theEntry; 317 } 318 } 319 320 static List<SingleValidationMessage> buildValidationMessages(List<ConcurrentValidationTask> validationTasks) { 321 List<SingleValidationMessage> retval = new ArrayList<>(); 322 try { 323 for (ConcurrentValidationTask validationTask : validationTasks) { 324 ValidationResult result = validationTask.getFuture().get(); 325 final String bundleEntryPathPrefix = validationTask.getResourcePathPrefix(); 326 List<SingleValidationMessage> messages = result.getMessages().stream() 327 .map(message -> { 328 String currentPath; 329 330 String locationString = StringUtils.defaultIfEmpty(message.getLocationString(), ""); 331 332 int dotIndex = locationString.indexOf('.'); 333 if (dotIndex >= 0) { 334 currentPath = locationString.substring(dotIndex); 335 } else { 336 if (isBlank(bundleEntryPathPrefix) || isBlank(locationString)) { 337 currentPath = locationString; 338 } else { 339 currentPath = "." + locationString; 340 } 341 } 342 343 message.setLocationString(bundleEntryPathPrefix + currentPath); 344 return message; 345 }) 346 .collect(Collectors.toList()); 347 retval.addAll(messages); 348 } 349 } catch (InterruptedException | ExecutionException exp) { 350 throw new InternalErrorException(Msg.code(2246) + exp); 351 } 352 return retval; 353 } 354 355 private ValidationResult validateResource(IValidationContext<IBaseResource> theValidationContext) { 356 for (IValidatorModule next : myValidators) { 357 next.validateResource(theValidationContext); 358 } 359 return theValidationContext.toResult(); 360 } 361 362 private ValidationResult invokeValidationCompletedHooks( 363 IBaseResource theResourceParsed, String theResourceRaw, ValidationResult theValidationResult) { 364 if (myInterceptorBroadcaster != null) { 365 if (myInterceptorBroadcaster.hasHooks(Pointcut.VALIDATION_COMPLETED)) { 366 HookParams params = new HookParams() 367 .add(IBaseResource.class, theResourceParsed) 368 .add(String.class, theResourceRaw) 369 .add(ValidationResult.class, theValidationResult); 370 Object newResult = 371 myInterceptorBroadcaster.callHooksAndReturnObject(Pointcut.VALIDATION_COMPLETED, params); 372 if (newResult != null) { 373 theValidationResult = (ValidationResult) newResult; 374 } 375 } 376 } 377 return theValidationResult; 378 } 379 380 /** 381 * Optionally supplies an interceptor broadcaster that will be used to invoke validation related Pointcut events 382 * 383 * @since 5.5.0 384 */ 385 public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBraodcaster) { 386 myInterceptorBroadcaster = theInterceptorBraodcaster; 387 } 388 389 public FhirValidator setExecutorService(ExecutorService theExecutorService) { 390 myExecutorService = theExecutorService; 391 return this; 392 } 393 394 /** 395 * If this is true, bundles will be validated in parallel threads. The bundle structure itself will not be validated, 396 * only the resources in its entries. 397 */ 398 public boolean isConcurrentBundleValidation() { 399 return myConcurrentBundleValidation; 400 } 401 402 /** 403 * If this is true, bundles will be validated in parallel threads. The bundle structure itself will not be validated, 404 * only the resources in its entries. 405 */ 406 public FhirValidator setConcurrentBundleValidation(boolean theConcurrentBundleValidation) { 407 myConcurrentBundleValidation = theConcurrentBundleValidation; 408 return this; 409 } 410 411 /** 412 * If this is true, any resource that has contained resources will first be deep-copied and then the contained 413 * resources remove from the copy and this copy without contained resources will be validated. 414 */ 415 public boolean isSkipContainedReferenceValidation() { 416 return mySkipContainedReferenceValidation; 417 } 418 419 /** 420 * If this is true, any resource that has contained resources will first be deep-copied and then the contained 421 * resources remove from the copy and this copy without contained resources will be validated. 422 */ 423 public FhirValidator setSkipContainedReferenceValidation(boolean theSkipContainedReferenceValidation) { 424 mySkipContainedReferenceValidation = theSkipContainedReferenceValidation; 425 return this; 426 } 427 428 // Simple Tuple to keep track of bundle path and associate aync future task 429 static class ConcurrentValidationTask { 430 private final String myResourcePathPrefix; 431 private final Future<ValidationResult> myFuture; 432 433 ConcurrentValidationTask(String theResourcePathPrefix, Future<ValidationResult> theFuture) { 434 myResourcePathPrefix = theResourcePathPrefix; 435 myFuture = theFuture; 436 } 437 438 public String getResourcePathPrefix() { 439 return myResourcePathPrefix; 440 } 441 442 public Future<ValidationResult> getFuture() { 443 return myFuture; 444 } 445 } 446}