package ca.uhn.fhir.rest.server.interceptor; /* * #%L * HAPI FHIR - Core Library * %% * Copyright (C) 2014 - 2017 University Health Network * %% * 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% */ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.OperationOutcomeUtil; import ca.uhn.fhir.validation.*; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.text.StrLookup; import org.apache.commons.lang3.text.StrSubstitutor; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import java.util.ArrayList; import java.util.List; import static org.apache.commons.lang3.StringUtils.isNotBlank; /** * This interceptor intercepts each incoming request and if it contains a FHIR resource, validates that resource. The * interceptor may be configured to run any validator modules, and will then add headers to the response or fail the * request with an {@link UnprocessableEntityException HTTP 422 Unprocessable Entity}. */ abstract class BaseValidatingInterceptor<T> extends InterceptorAdapter { /** * Default value:<br/> * <code> * ${row}:${col} ${severity} ${message} (${location}) * </code> */ public static final String DEFAULT_RESPONSE_HEADER_VALUE = "${row}:${col} ${severity} ${message} (${location})"; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseValidatingInterceptor.class); private Integer myAddResponseIssueHeaderOnSeverity = null; private Integer myAddResponseOutcomeHeaderOnSeverity = null; private Integer myFailOnSeverity = ResultSeverityEnum.ERROR.ordinal(); private boolean myIgnoreValidatorExceptions; private int myMaximumHeaderLength = 200; private String myResponseIssueHeaderName = provideDefaultResponseHeaderName(); private String myResponseIssueHeaderValue = DEFAULT_RESPONSE_HEADER_VALUE; private String myResponseIssueHeaderValueNoIssues = null; private String myResponseOutcomeHeaderName = provideDefaultResponseHeaderName(); private List<IValidatorModule> myValidatorModules; private void addResponseIssueHeader(RequestDetails theRequestDetails, SingleValidationMessage theNext) { // Perform any string substitutions from the message format StrLookup<?> lookup = new MyLookup(theNext); StrSubstitutor subs = new StrSubstitutor(lookup, "${", "}", '\\'); // Log the header String headerValue = subs.replace(myResponseIssueHeaderValue); ourLog.trace("Adding header to response: {}", headerValue); theRequestDetails.getResponse().addHeader(myResponseIssueHeaderName, headerValue); } public BaseValidatingInterceptor<T> addValidatorModule(IValidatorModule theModule) { Validate.notNull(theModule, "theModule must not be null"); if (getValidatorModules() == null) { setValidatorModules(new ArrayList<IValidatorModule>()); } getValidatorModules().add(theModule); return this; } abstract ValidationResult doValidate(FhirValidator theValidator, T theRequest); /** * Fail the request by throwing an {@link UnprocessableEntityException} as a result of a validation failure. * Subclasses may change this behaviour by providing alternate behaviour. */ protected void fail(RequestDetails theRequestDetails, ValidationResult theValidationResult) { throw new UnprocessableEntityException(theRequestDetails.getServer().getFhirContext(), theValidationResult.toOperationOutcome()); } /** * 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 ResultSeverityEnum getAddResponseOutcomeHeaderOnSeverity() { return myAddResponseOutcomeHeaderOnSeverity != null ? ResultSeverityEnum.values()[myAddResponseOutcomeHeaderOnSeverity] : null; } /** * The maximum length for an individual header. If an individual header would be written exceeding this length, * the header value will be truncated. */ public int getMaximumHeaderLength() { return myMaximumHeaderLength; } /** * The name of the header specified by {@link #setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum)} */ public String getResponseOutcomeHeaderName() { return myResponseOutcomeHeaderName; } public List<IValidatorModule> getValidatorModules() { return myValidatorModules; } /** * 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 * any exceptions. * <p> * This setting is mostly useful in testing situations * </p> */ public boolean isIgnoreValidatorExceptions() { 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 * will exit immediately and allow processing to continue if the validator throws * any exceptions. * <p> * This setting is mostly useful in testing situations * </p> */ public void setIgnoreValidatorExceptions(boolean theIgnoreValidatorExceptions) { myIgnoreValidatorExceptions = theIgnoreValidatorExceptions; } /** * 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; } /** * Sets the name of the response header to add validation failures to * * @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum) */ protected void setResponseHeaderName(String theResponseHeaderName) { Validate.notBlank(theResponseHeaderName, "theResponseHeaderName must not be blank or null"); myResponseIssueHeaderName = theResponseHeaderName; } /** * Sets the value to add to the response header with the name specified by {@link #setResponseHeaderName(String)} * when validation produces a message of severity equal to or greater than * {@link #setAddResponseHeaderOnSeverity(ResultSeverityEnum)} * <p> * This field allows the following substitutions: * </p> * <table> * <tr> * <td>Name</td> * <td>Value</td> * </tr> * <tr> * <td>${line}</td> * <td>The line in the request</td> * </tr> * <tr> * <td>${col}</td> * <td>The column in the request</td> * </tr> * <tr> * <td>${location}</td> * <td>The location in the payload as a string (typically this will be a path)</td> * </tr> * <tr> * <td>${severity}</td> * <td>The severity of the issue</td> * </tr> * <tr> * <td>${message}</td> * <td>The validation message</td> * </tr> * </table> * * @see #DEFAULT_RESPONSE_HEADER_VALUE * @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum) */ public void setResponseHeaderValue(String theResponseHeaderValue) { Validate.notBlank(theResponseHeaderValue, "theResponseHeaderValue must not be blank or null"); myResponseIssueHeaderValue = theResponseHeaderValue; } /** * Sets the header value to add when no issues are found at or exceeding the * threshold specified by {@link #setAddResponseHeaderOnSeverity(ResultSeverityEnum)} */ public void setResponseHeaderValueNoIssues(String 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 * ValidationResult). */ protected void postProcessResult(RequestDetails theRequestDetails, ValidationResult theValidationResult) { } /** * Hook for subclasses on failure (e.g. add a response header to an incoming resource upon rejection). */ protected void postProcessResultOnFailure(RequestDetails theRequestDetails, ValidationResult theValidationResult) { } /** * Note: May return null */ protected ValidationResult validate(T theRequest, RequestDetails theRequestDetails) { FhirValidator validator = theRequestDetails.getServer().getFhirContext().newValidator(); if (myValidatorModules != null) { for (IValidatorModule next : myValidatorModules) { validator.registerValidatorModule(next); } } if (theRequest == null) { return null; } ValidationResult validationResult; try { validationResult = doValidate(validator, theRequest); } catch (Exception e) { if (myIgnoreValidatorExceptions) { ourLog.warn("Validator threw an exception during validation", e); return null; } if (e instanceof BaseServerResponseException) { throw (BaseServerResponseException)e; } throw new InternalErrorException(e); } if (myAddResponseIssueHeaderOnSeverity != null) { boolean found = false; for (SingleValidationMessage next : validationResult.getMessages()) { if (next.getSeverity().ordinal() >= myAddResponseIssueHeaderOnSeverity) { addResponseIssueHeader(theRequestDetails, next); found = true; } } if (!found) { if (isNotBlank(myResponseIssueHeaderValueNoIssues)) { theRequestDetails.getResponse().addHeader(myResponseIssueHeaderName, myResponseIssueHeaderValueNoIssues); } } } if (myFailOnSeverity != null) { for (SingleValidationMessage next : validationResult.getMessages()) { if (next.getSeverity().ordinal() >= myFailOnSeverity) { postProcessResultOnFailure(theRequestDetails, validationResult); fail(theRequestDetails, validationResult); return validationResult; } } } if (myAddResponseOutcomeHeaderOnSeverity != null) { IBaseOperationOutcome outcome = null; for (SingleValidationMessage next : validationResult.getMessages()) { if (next.getSeverity().ordinal() >= myAddResponseOutcomeHeaderOnSeverity) { outcome = validationResult.toOperationOutcome(); break; } } if (outcome == null && myAddResponseOutcomeHeaderOnSeverity != null && myAddResponseOutcomeHeaderOnSeverity == ResultSeverityEnum.INFORMATION.ordinal()) { FhirContext ctx = theRequestDetails.getServer().getFhirContext(); outcome = OperationOutcomeUtil.newInstance(ctx); OperationOutcomeUtil.addIssue(ctx, outcome, "information", "No issues detected", "", "informational"); } if (outcome != null) { IParser parser = theRequestDetails.getServer().getFhirContext().newJsonParser().setPrettyPrint(false); String encoded = parser.encodeResourceToString(outcome); if (encoded.length() > getMaximumHeaderLength()) { encoded = encoded.substring(0, getMaximumHeaderLength() - 3) + "..."; } theRequestDetails.getResponse().addHeader(myResponseOutcomeHeaderName, encoded); } } postProcessResult(theRequestDetails, validationResult); return validationResult; } private static class MyLookup extends StrLookup<String> { private SingleValidationMessage myMessage; public MyLookup(SingleValidationMessage theMessage) { myMessage = theMessage; } @Override public String lookup(String theKey) { if ("line".equals(theKey)) { return toString(myMessage.getLocationLine()); } if ("col".equals(theKey)) { return toString(myMessage.getLocationCol()); } if ("message".equals(theKey)) { return toString(myMessage.getMessage()); } if ("location".equals(theKey)) { return toString(myMessage.getLocationString()); } if ("severity".equals(theKey)) { return myMessage.getSeverity() != null ? myMessage.getSeverity().name() : null; } return ""; } private static String toString(Object theInt) { return theInt != null ? theInt.toString() : ""; } } }