Update endpoint validation interceptors (#2698)

This commit is contained in:
James Agnew 2021-06-02 05:39:01 -04:00 committed by GitHub
parent fe763dd2d6
commit 1d2b47c9fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 101 additions and 86 deletions

View File

@ -271,7 +271,7 @@ public class FhirValidator {
* *
* @since 5.5.0 * @since 5.5.0
*/ */
public void setInterceptorBraodcaster(IInterceptorBroadcaster theInterceptorBraodcaster) { public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBraodcaster) {
myInterceptorBraodcaster = theInterceptorBraodcaster; myInterceptorBraodcaster = theInterceptorBraodcaster;
} }
} }

View File

@ -1717,7 +1717,7 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
FhirValidator validator = getContext().newValidator(); FhirValidator validator = getContext().newValidator();
validator.setInterceptorBraodcaster(CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest)); validator.setInterceptorBroadcaster(CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest));
validator.registerValidatorModule(getInstanceValidator()); validator.registerValidatorModule(getInstanceValidator());
validator.registerValidatorModule(new IdChecker(theMode)); validator.registerValidatorModule(new IdChecker(theMode));

View File

@ -68,7 +68,7 @@ class RequireValidationRule extends BaseTypedRule {
public RuleEvaluation evaluate(RequestDetails theRequestDetails, @Nonnull IBaseResource theResource) { public RuleEvaluation evaluate(RequestDetails theRequestDetails, @Nonnull IBaseResource theResource) {
FhirValidator validator = getFhirContext().newValidator(); FhirValidator validator = getFhirContext().newValidator();
validator.setInterceptorBraodcaster(CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails)); validator.setInterceptorBroadcaster(CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails));
validator.registerValidatorModule(myValidator); validator.registerValidatorModule(myValidator);
ValidationResult outcome = validator.validateWithResult(theResource); ValidationResult outcome = validator.validateWithResult(theResource);

View File

@ -8,15 +8,13 @@ import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.validation.ValidationMessageSuppressingInterceptor; import ca.uhn.fhir.rest.server.interceptor.validation.ValidationMessageSuppressingInterceptor;
import ca.uhn.fhir.validation.IValidatorModule; import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhir.validation.ResultSeverityEnum;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r5.utils.IResourceValidator; import org.hl7.fhir.r5.utils.IResourceValidator;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
@ -82,11 +80,13 @@ public class ValidationMessageSuppressingInterceptorTest extends BaseResourcePro
upload("/r4/uscore/CodeSystem-dummy-loinc.json"); upload("/r4/uscore/CodeSystem-dummy-loinc.json");
upload("/r4/uscore/StructureDefinition-us-core-pulse-oximetry.json"); upload("/r4/uscore/StructureDefinition-us-core-pulse-oximetry.json");
FhirValidator validator = myFhirCtx.newValidator();
validator.setInterceptorBroadcaster(myInterceptorRegistry);
validator.registerValidatorModule(new FhirInstanceValidator(myValidationSupport));
RequestValidatingInterceptor requestInterceptor = new RequestValidatingInterceptor(); RequestValidatingInterceptor requestInterceptor = new RequestValidatingInterceptor();
requestInterceptor.setFailOnSeverity(ResultSeverityEnum.ERROR); requestInterceptor.setFailOnSeverity(ResultSeverityEnum.ERROR);
requestInterceptor.setValidatorModules(Collections.singletonList(new FhirInstanceValidator(myValidationSupport))); requestInterceptor.setValidator(validator);
requestInterceptor.setInterceptorBroadcaster(myInterceptorRegistry);
ourRestServer.registerInterceptor(requestInterceptor); ourRestServer.registerInterceptor(requestInterceptor);

View File

@ -21,17 +21,18 @@ package ca.uhn.fhir.rest.server.interceptor;
*/ */
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Interceptor; import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.util.OperationOutcomeUtil; import ca.uhn.fhir.util.OperationOutcomeUtil;
import ca.uhn.fhir.validation.*; import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.IValidatorModule;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import ca.uhn.fhir.validation.SingleValidationMessage;
import ca.uhn.fhir.validation.ValidationResult;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.text.StrLookup; import org.apache.commons.lang3.text.StrLookup;
import org.apache.commons.lang3.text.StrSubstitutor; import org.apache.commons.lang3.text.StrSubstitutor;
@ -73,7 +74,7 @@ public abstract class BaseValidatingInterceptor<T> extends ValidationResultEnric
private String myResponseOutcomeHeaderName = provideDefaultResponseHeaderName(); private String myResponseOutcomeHeaderName = provideDefaultResponseHeaderName();
private List<IValidatorModule> myValidatorModules; private List<IValidatorModule> myValidatorModules;
private IInterceptorBroadcaster myInterceptorBroadcaster; private FhirValidator myValidator;
private void addResponseIssueHeader(RequestDetails theRequestDetails, SingleValidationMessage theNext) { private void addResponseIssueHeader(RequestDetails theRequestDetails, SingleValidationMessage theNext) {
// Perform any string substitutions from the message format // Perform any string substitutions from the message format
@ -87,15 +88,33 @@ public abstract class BaseValidatingInterceptor<T> extends ValidationResultEnric
theRequestDetails.getResponse().addHeader(myResponseIssueHeaderName, headerValue); theRequestDetails.getResponse().addHeader(myResponseIssueHeaderName, headerValue);
} }
/**
* Specify a validator module to use.
*
* @see #setValidator(FhirValidator)
*/
public BaseValidatingInterceptor<T> addValidatorModule(IValidatorModule theModule) { public BaseValidatingInterceptor<T> addValidatorModule(IValidatorModule theModule) {
Validate.notNull(theModule, "theModule must not be null"); Validate.notNull(theModule, "theModule must not be null");
Validate.isTrue(myValidator == null, "Can not specify both a validator and validator modules. Only one needs to be supplied.");
if (getValidatorModules() == null) { if (getValidatorModules() == null) {
setValidatorModules(new ArrayList<IValidatorModule>()); setValidatorModules(new ArrayList<>());
} }
getValidatorModules().add(theModule); getValidatorModules().add(theModule);
return this; return this;
} }
/**
* Provides the validator to use. This can be used as an alternative to {@link #addValidatorModule(IValidatorModule)}
*
* @see #addValidatorModule(IValidatorModule)
* @see #setValidatorModules(List)
*/
public void setValidator(FhirValidator theValidator) {
Validate.isTrue(theValidator == null || getValidatorModules() == null || getValidatorModules().isEmpty(), "Can not specify both a validator and validator modules. Only one needs to be supplied.");
myValidator = theValidator;
}
abstract ValidationResult doValidate(FhirValidator theValidator, T theRequest); abstract ValidationResult doValidate(FhirValidator theValidator, T theRequest);
/** /**
@ -115,6 +134,15 @@ public abstract class BaseValidatingInterceptor<T> extends ValidationResultEnric
return myAddResponseOutcomeHeaderOnSeverity != null ? ResultSeverityEnum.values()[myAddResponseOutcomeHeaderOnSeverity] : null; return myAddResponseOutcomeHeaderOnSeverity != null ? ResultSeverityEnum.values()[myAddResponseOutcomeHeaderOnSeverity] : null;
} }
/**
* If the validation produces a result with at least the given severity, a header with the name
* specified by {@link #setResponseOutcomeHeaderName(String)} will be added containing a JSON encoded
* OperationOutcome resource containing the validation results.
*/
public void setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum theAddResponseOutcomeHeaderOnSeverity) {
myAddResponseOutcomeHeaderOnSeverity = theAddResponseOutcomeHeaderOnSeverity != null ? theAddResponseOutcomeHeaderOnSeverity.ordinal() : null;
}
/** /**
* The maximum length for an individual header. If an individual header would be written exceeding this length, * The maximum length for an individual header. If an individual header would be written exceeding this length,
* the header value will be truncated. * the header value will be truncated.
@ -123,6 +151,15 @@ public abstract class BaseValidatingInterceptor<T> extends ValidationResultEnric
return myMaximumHeaderLength; return myMaximumHeaderLength;
} }
/**
* The maximum length for an individual header. If an individual header would be written exceeding this length,
* the header value will be truncated. Value must be greater than 100.
*/
public void setMaximumHeaderLength(int theMaximumHeaderLength) {
Validate.isTrue(theMaximumHeaderLength >= 100, "theMaximumHeadeerLength must be >= 100");
myMaximumHeaderLength = theMaximumHeaderLength;
}
/** /**
* The name of the header specified by {@link #setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum)} * The name of the header specified by {@link #setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum)}
*/ */
@ -130,10 +167,23 @@ public abstract class BaseValidatingInterceptor<T> extends ValidationResultEnric
return myResponseOutcomeHeaderName; return myResponseOutcomeHeaderName;
} }
/**
* The name of the header specified by {@link #setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum)}
*/
public void setResponseOutcomeHeaderName(String theResponseOutcomeHeaderName) {
Validate.notEmpty(theResponseOutcomeHeaderName, "theResponseOutcomeHeaderName can not be empty or null");
myResponseOutcomeHeaderName = theResponseOutcomeHeaderName;
}
public List<IValidatorModule> getValidatorModules() { public List<IValidatorModule> getValidatorModules() {
return myValidatorModules; return myValidatorModules;
} }
public void setValidatorModules(List<IValidatorModule> theValidatorModules) {
Validate.isTrue(myValidator == null || theValidatorModules == null || theValidatorModules.isEmpty(), "Can not specify both a validator and validator modules. Only one needs to be supplied.");
myValidatorModules = theValidatorModules;
}
/** /**
* If set to <code>true</code> (default is <code>false</code>) this interceptor * If set to <code>true</code> (default is <code>false</code>) this interceptor
* will exit immediately and allow processing to continue if the validator throws * will exit immediately and allow processing to continue if the validator throws
@ -146,36 +196,6 @@ public abstract class BaseValidatingInterceptor<T> extends ValidationResultEnric
return myIgnoreValidatorExceptions; return myIgnoreValidatorExceptions;
} }
abstract String provideDefaultResponseHeaderName();
/**
* Sets the minimum severity at which an issue detected by the validator will result in a header being added to the
* response. Default is {@link ResultSeverityEnum#INFORMATION}. Set to <code>null</code> to disable this behaviour.
*
* @see #setResponseHeaderName(String)
* @see #setResponseHeaderValue(String)
*/
public void setAddResponseHeaderOnSeverity(ResultSeverityEnum theSeverity) {
myAddResponseIssueHeaderOnSeverity = theSeverity != null ? theSeverity.ordinal() : null;
}
/**
* If the validation produces a result with at least the given severity, a header with the name
* specified by {@link #setResponseOutcomeHeaderName(String)} will be added containing a JSON encoded
* OperationOutcome resource containing the validation results.
*/
public void setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum theAddResponseOutcomeHeaderOnSeverity) {
myAddResponseOutcomeHeaderOnSeverity = theAddResponseOutcomeHeaderOnSeverity != null ? theAddResponseOutcomeHeaderOnSeverity.ordinal() : null;
}
/**
* Sets the minimum severity at which an issue detected by the validator will fail/reject the request. Default is
* {@link ResultSeverityEnum#ERROR}. Set to <code>null</code> to disable this behaviour.
*/
public void setFailOnSeverity(ResultSeverityEnum theSeverity) {
myFailOnSeverity = theSeverity != null ? theSeverity.ordinal() : null;
}
/** /**
* If set to <code>true</code> (default is <code>false</code>) this interceptor * If set to <code>true</code> (default is <code>false</code>) this interceptor
* will exit immediately and allow processing to continue if the validator throws * will exit immediately and allow processing to continue if the validator throws
@ -188,18 +208,30 @@ public abstract class BaseValidatingInterceptor<T> extends ValidationResultEnric
myIgnoreValidatorExceptions = theIgnoreValidatorExceptions; myIgnoreValidatorExceptions = theIgnoreValidatorExceptions;
} }
abstract String provideDefaultResponseHeaderName();
/** /**
* The maximum length for an individual header. If an individual header would be written exceeding this length, * Sets the minimum severity at which an issue detected by the validator will result in a header being added to the
* the header value will be truncated. Value must be greater than 100. * response. Default is {@link ResultSeverityEnum#INFORMATION}. Set to <code>null</code> to disable this behaviour.
*
* @see #setResponseHeaderName(String)
* @see #setResponseHeaderValue(String)
*/ */
public void setMaximumHeaderLength(int theMaximumHeaderLength) { public void setAddResponseHeaderOnSeverity(ResultSeverityEnum theSeverity) {
Validate.isTrue(theMaximumHeaderLength >= 100, "theMaximumHeadeerLength must be >= 100"); myAddResponseIssueHeaderOnSeverity = theSeverity != null ? theSeverity.ordinal() : null;
myMaximumHeaderLength = theMaximumHeaderLength; }
/**
* Sets the minimum severity at which an issue detected by the validator will fail/reject the request. Default is
* {@link ResultSeverityEnum#ERROR}. Set to <code>null</code> to disable this behaviour.
*/
public void setFailOnSeverity(ResultSeverityEnum theSeverity) {
myFailOnSeverity = theSeverity != null ? theSeverity.ordinal() : null;
} }
/** /**
* Sets the name of the response header to add validation failures to * Sets the name of the response header to add validation failures to
* *
* @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum) * @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum)
*/ */
protected void setResponseHeaderName(String theResponseHeaderName) { protected void setResponseHeaderName(String theResponseHeaderName) {
@ -240,7 +272,7 @@ public abstract class BaseValidatingInterceptor<T> extends ValidationResultEnric
* <td>The validation message</td> * <td>The validation message</td>
* </tr> * </tr>
* </table> * </table>
* *
* @see #DEFAULT_RESPONSE_HEADER_VALUE * @see #DEFAULT_RESPONSE_HEADER_VALUE
* @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum) * @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum)
*/ */
@ -257,48 +289,39 @@ public abstract class BaseValidatingInterceptor<T> extends ValidationResultEnric
myResponseIssueHeaderValueNoIssues = theResponseHeaderValueNoIssues; myResponseIssueHeaderValueNoIssues = theResponseHeaderValueNoIssues;
} }
/**
* The name of the header specified by {@link #setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum)}
*/
public void setResponseOutcomeHeaderName(String theResponseOutcomeHeaderName) {
Validate.notEmpty(theResponseOutcomeHeaderName, "theResponseOutcomeHeaderName can not be empty or null");
myResponseOutcomeHeaderName = theResponseOutcomeHeaderName;
}
public void setValidatorModules(List<IValidatorModule> theValidatorModules) {
myValidatorModules = theValidatorModules;
}
/** /**
* Hook for subclasses (e.g. add a tag (coding) to an incoming resource when a given severity appears in the * Hook for subclasses (e.g. add a tag (coding) to an incoming resource when a given severity appears in the
* ValidationResult). * ValidationResult).
*/ */
protected void postProcessResult(RequestDetails theRequestDetails, ValidationResult theValidationResult) { } protected void postProcessResult(RequestDetails theRequestDetails, ValidationResult theValidationResult) {
}
/** /**
* Hook for subclasses on failure (e.g. add a response header to an incoming resource upon rejection). * Hook for subclasses on failure (e.g. add a response header to an incoming resource upon rejection).
*/ */
protected void postProcessResultOnFailure(RequestDetails theRequestDetails, ValidationResult theValidationResult) { } protected void postProcessResultOnFailure(RequestDetails theRequestDetails, ValidationResult theValidationResult) {
}
/** /**
* Note: May return null * Note: May return null
*/ */
protected ValidationResult validate(T theRequest, RequestDetails theRequestDetails) { protected ValidationResult validate(T theRequest, RequestDetails theRequestDetails) {
FhirValidator validator = theRequestDetails.getServer().getFhirContext().newValidator();
IInterceptorBroadcaster interceptorBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails);
validator.setInterceptorBraodcaster(interceptorBroadcaster);
if (myValidatorModules != null) {
for (IValidatorModule next : myValidatorModules) {
validator.registerValidatorModule(next);
}
}
if (theRequest == null) { if (theRequest == null) {
return null; return null;
} }
FhirValidator validator;
if (myValidator != null) {
validator = myValidator;
} else {
validator = theRequestDetails.getServer().getFhirContext().newValidator();
if (myValidatorModules != null) {
for (IValidatorModule next : myValidatorModules) {
validator.registerValidatorModule(next);
}
}
}
ValidationResult validationResult; ValidationResult validationResult;
try { try {
validationResult = doValidate(validator, theRequest); validationResult = doValidate(validator, theRequest);
@ -308,7 +331,7 @@ public abstract class BaseValidatingInterceptor<T> extends ValidationResultEnric
return null; return null;
} }
if (e instanceof BaseServerResponseException) { if (e instanceof BaseServerResponseException) {
throw (BaseServerResponseException)e; throw (BaseServerResponseException) e;
} }
throw new InternalErrorException(e); throw new InternalErrorException(e);
} }
@ -363,16 +386,8 @@ public abstract class BaseValidatingInterceptor<T> extends ValidationResultEnric
} }
postProcessResult(theRequestDetails, validationResult); postProcessResult(theRequestDetails, validationResult);
return validationResult;
}
/** return validationResult;
* This can be used to specify an interceptor to broadcast validation events through. This
* is mostly intended for the
*/
public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBroadcaster) {
myInterceptorBroadcaster = theInterceptorBroadcaster;
} }
private static class MyLookup extends StrLookup<String> { private static class MyLookup extends StrLookup<String> {