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