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