All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ca.uhn.fhir.validation.FhirValidator Maven / Gradle / Ivy

There is a newer version: 7.4.5
Show newest version
/*
 * #%L
 * HAPI FHIR - Core Library
 * %%
 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package ca.uhn.fhir.validation;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.TerserUtil;
import ca.uhn.fhir.validation.schematron.SchematronProvider;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static org.apache.commons.lang3.StringUtils.isBlank;

/**
 * Resource validator, which checks resources for compliance against various validation schemes (schemas, schematrons, profiles, etc.)
 *
 * 

* To obtain a resource validator, call {@link FhirContext#newValidator()} *

* *

* Thread safety note: 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 * them with caution. *

*/ public class FhirValidator { private static final Logger ourLog = LoggerFactory.getLogger(FhirValidator.class); private static final String I18N_KEY_NO_PH_ERROR = FhirValidator.class.getName() + ".noPhError"; private static volatile Boolean ourPhPresentOnClasspath; private final FhirContext myContext; private List myValidators = new ArrayList<>(); private IInterceptorBroadcaster myInterceptorBroadcaster; private boolean myConcurrentBundleValidation; private boolean mySkipContainedReferenceValidation; private ExecutorService myExecutorService; /** * Constructor (this should not be called directly, but rather {@link FhirContext#newValidator()} should be called to obtain an instance of {@link FhirValidator}) */ public FhirValidator(FhirContext theFhirContext) { myContext = theFhirContext; if (ourPhPresentOnClasspath == null) { ourPhPresentOnClasspath = SchematronProvider.isSchematronAvailable(theFhirContext); } } private void addOrRemoveValidator( boolean theValidateAgainstStandardSchema, Class type, IValidatorModule theInstance) { if (theValidateAgainstStandardSchema) { boolean found = haveValidatorOfType(type); if (!found) { registerValidatorModule(theInstance); } } else { for (IValidatorModule next : myValidators) { if (next.getClass().equals(type)) { unregisterValidatorModule(next); } } } } private boolean haveValidatorOfType(Class type) { boolean found = false; for (IValidatorModule next : myValidators) { if (next.getClass().equals(type)) { found = true; break; } } return found; } /** * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself) */ public synchronized boolean isValidateAgainstStandardSchema() { return haveValidatorOfType(SchemaBaseValidator.class); } /** * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself) * * @return Returns a referens to this for method chaining */ public synchronized FhirValidator setValidateAgainstStandardSchema(boolean theValidateAgainstStandardSchema) { addOrRemoveValidator( theValidateAgainstStandardSchema, SchemaBaseValidator.class, new SchemaBaseValidator(myContext)); return this; } /** * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself) */ public synchronized boolean isValidateAgainstStandardSchematron() { if (!ourPhPresentOnClasspath) { // No need to ask since we dont have Ph-Schematron. Also Class.forname will complain // about missing ph-schematron import. return false; } Class cls = SchematronProvider.getSchematronValidatorClass(); return haveValidatorOfType(cls); } /** * Should the validator validate the resource against the base schematron (the schematron provided with the FHIR distribution itself) * * @return Returns a referens to this for method chaining */ public synchronized FhirValidator setValidateAgainstStandardSchematron( boolean theValidateAgainstStandardSchematron) { if (theValidateAgainstStandardSchematron && !ourPhPresentOnClasspath) { throw new IllegalArgumentException( Msg.code(1970) + myContext.getLocalizer().getMessage(I18N_KEY_NO_PH_ERROR)); } if (!theValidateAgainstStandardSchematron && !ourPhPresentOnClasspath) { return this; } Class cls = SchematronProvider.getSchematronValidatorClass(); IValidatorModule instance = SchematronProvider.getSchematronValidatorInstance(myContext); addOrRemoveValidator(theValidateAgainstStandardSchematron, cls, instance); return this; } /** * Add a new validator module to this validator. You may register as many modules as you like at any time. * * @param theValidator The validator module. Must not be null. * @return Returns a reference to this for easy method chaining. */ public synchronized FhirValidator registerValidatorModule(IValidatorModule theValidator) { Validate.notNull(theValidator, "theValidator must not be null"); ArrayList newValidators = new ArrayList<>(myValidators.size() + 1); newValidators.addAll(myValidators); newValidators.add(theValidator); myValidators = newValidators; return this; } /** * Removes a validator module from this validator. You may register as many modules as you like, and remove them at any time. * * @param theValidator The validator module. Must not be null. */ public synchronized void unregisterValidatorModule(IValidatorModule theValidator) { Validate.notNull(theValidator, "theValidator must not be null"); ArrayList newValidators = new ArrayList(myValidators.size() + 1); newValidators.addAll(myValidators); newValidators.remove(theValidator); myValidators = newValidators; } private void applyDefaultValidators() { if (myValidators.isEmpty()) { setValidateAgainstStandardSchema(true); if (ourPhPresentOnClasspath) { setValidateAgainstStandardSchematron(true); } } } /** * Validates a resource instance returning a {@link ValidationResult} which contains the results. * * @param theResource the resource to validate * @return the results of validation * @since 0.7 */ public ValidationResult validateWithResult(IBaseResource theResource) { return validateWithResult(theResource, null); } /** * Validates a resource instance returning a {@link ValidationResult} which contains the results. * * @param theResource the resource to validate * @return the results of validation * @since 1.1 */ public ValidationResult validateWithResult(String theResource) { return validateWithResult(theResource, null); } /** * Validates a resource instance returning a {@link ValidationResult} which contains the results. * * @param theResource the resource to validate * @param theOptions Optionally provides options to the validator * @return the results of validation * @since 4.0.0 */ public ValidationResult validateWithResult(String theResource, ValidationOptions theOptions) { Validate.notNull(theResource, "theResource must not be null"); IValidationContext validationContext = ValidationContext.forText(myContext, theResource, theOptions); Function callback = result -> invokeValidationCompletedHooks(null, theResource, result); return doValidate(validationContext, theOptions, callback); } /** * Validates a resource instance returning a {@link ValidationResult} which contains the results. * * @param theResource the resource to validate * @param theOptions Optionally provides options to the validator * @return the results of validation * @since 4.0.0 */ public ValidationResult validateWithResult(IBaseResource theResource, ValidationOptions theOptions) { Validate.notNull(theResource, "theResource must not be null"); IValidationContext validationContext = ValidationContext.forResource(myContext, theResource, theOptions); Function callback = result -> invokeValidationCompletedHooks(theResource, null, result); return doValidate(validationContext, theOptions, callback); } private ValidationResult doValidate( IValidationContext theValidationContext, ValidationOptions theOptions, Function theValidationCompletionCallback) { applyDefaultValidators(); ValidationResult result; if (myConcurrentBundleValidation && theValidationContext.getResource() instanceof IBaseBundle && myExecutorService != null) { result = validateBundleEntriesConcurrently(theValidationContext, theOptions); } else { result = validateResource(theValidationContext); } return theValidationCompletionCallback.apply(result); } private ValidationResult validateBundleEntriesConcurrently( IValidationContext theValidationContext, ValidationOptions theOptions) { List entries = BundleUtil.toListOfResources(myContext, (IBaseBundle) theValidationContext.getResource()); // Async validation tasks List validationTasks = IntStream.range(0, entries.size()) .mapToObj(index -> { IBaseResource resourceToValidate; IBaseResource entry = entries.get(index); if (mySkipContainedReferenceValidation) { resourceToValidate = withoutContainedResources(entry); } else { resourceToValidate = entry; } String entryPathPrefix = String.format("Bundle.entry[%d].resource.ofType(%s)", index, resourceToValidate.fhirType()); Future future = myExecutorService.submit(() -> { IValidationContext entryValidationContext = ValidationContext.forResource( theValidationContext.getFhirContext(), resourceToValidate, theOptions); return validateResource(entryValidationContext); }); return new ConcurrentValidationTask(entryPathPrefix, future); }) .collect(Collectors.toList()); List validationMessages = buildValidationMessages(validationTasks); return new ValidationResult(myContext, validationMessages); } IBaseResource withoutContainedResources(IBaseResource theEntry) { if (TerserUtil.hasValues(myContext, theEntry, "contained")) { IBaseResource deepCopy = TerserUtil.clone(myContext, theEntry); TerserUtil.clearField(myContext, deepCopy, "contained"); return deepCopy; } else { return theEntry; } } static List buildValidationMessages(List validationTasks) { List retval = new ArrayList<>(); try { for (ConcurrentValidationTask validationTask : validationTasks) { ValidationResult result = validationTask.getFuture().get(); final String bundleEntryPathPrefix = validationTask.getResourcePathPrefix(); List messages = result.getMessages().stream() .map(message -> { String currentPath; String locationString = StringUtils.defaultIfEmpty(message.getLocationString(), ""); int dotIndex = locationString.indexOf('.'); if (dotIndex >= 0) { currentPath = locationString.substring(dotIndex); } else { if (isBlank(bundleEntryPathPrefix) || isBlank(locationString)) { currentPath = locationString; } else { currentPath = "." + locationString; } } message.setLocationString(bundleEntryPathPrefix + currentPath); return message; }) .collect(Collectors.toList()); retval.addAll(messages); } } catch (InterruptedException | ExecutionException exp) { throw new InternalErrorException(Msg.code(2246) + exp); } return retval; } private ValidationResult validateResource(IValidationContext theValidationContext) { for (IValidatorModule next : myValidators) { next.validateResource(theValidationContext); } return theValidationContext.toResult(); } private ValidationResult invokeValidationCompletedHooks( IBaseResource theResourceParsed, String theResourceRaw, ValidationResult theValidationResult) { if (myInterceptorBroadcaster != null) { if (myInterceptorBroadcaster.hasHooks(Pointcut.VALIDATION_COMPLETED)) { HookParams params = new HookParams() .add(IBaseResource.class, theResourceParsed) .add(String.class, theResourceRaw) .add(ValidationResult.class, theValidationResult); Object newResult = myInterceptorBroadcaster.callHooksAndReturnObject(Pointcut.VALIDATION_COMPLETED, params); if (newResult != null) { theValidationResult = (ValidationResult) newResult; } } } return theValidationResult; } /** * Optionally supplies an interceptor broadcaster that will be used to invoke validation related Pointcut events * * @since 5.5.0 */ public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBraodcaster) { myInterceptorBroadcaster = theInterceptorBraodcaster; } public FhirValidator setExecutorService(ExecutorService theExecutorService) { myExecutorService = theExecutorService; return this; } /** * If this is true, bundles will be validated in parallel threads. The bundle structure itself will not be validated, * only the resources in its entries. */ public boolean isConcurrentBundleValidation() { return myConcurrentBundleValidation; } /** * If this is true, bundles will be validated in parallel threads. The bundle structure itself will not be validated, * only the resources in its entries. */ public FhirValidator setConcurrentBundleValidation(boolean theConcurrentBundleValidation) { myConcurrentBundleValidation = theConcurrentBundleValidation; return this; } /** * If this is true, any resource that has contained resources will first be deep-copied and then the contained * resources remove from the copy and this copy without contained resources will be validated. */ public boolean isSkipContainedReferenceValidation() { return mySkipContainedReferenceValidation; } /** * If this is true, any resource that has contained resources will first be deep-copied and then the contained * resources remove from the copy and this copy without contained resources will be validated. */ public FhirValidator setSkipContainedReferenceValidation(boolean theSkipContainedReferenceValidation) { mySkipContainedReferenceValidation = theSkipContainedReferenceValidation; return this; } // Simple Tuple to keep track of bundle path and associate aync future task static class ConcurrentValidationTask { private final String myResourcePathPrefix; private final Future myFuture; ConcurrentValidationTask(String theResourcePathPrefix, Future theFuture) { myResourcePathPrefix = theResourcePathPrefix; myFuture = theFuture; } public String getResourcePathPrefix() { return myResourcePathPrefix; } public Future getFuture() { return myFuture; } } }