ca.uhn.fhir.validation.FhirValidator Maven / Gradle / Ivy
/*
* #%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 extends IValidatorModule> 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 extends IValidatorModule> 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 extends IValidatorModule> 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 extends IValidatorModule> 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;
}
}
}