001package ca.uhn.fhir.rest.server.interceptor;
002
003/*
004 * #%L
005 * HAPI FHIR - Server Framework
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.i18n.Msg;
024import ca.uhn.fhir.context.FhirContext;
025import ca.uhn.fhir.interceptor.api.Interceptor;
026import ca.uhn.fhir.parser.IParser;
027import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
028import ca.uhn.fhir.rest.api.server.RequestDetails;
029import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
030import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
031import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
032import ca.uhn.fhir.util.OperationOutcomeUtil;
033import ca.uhn.fhir.validation.FhirValidator;
034import ca.uhn.fhir.validation.IValidatorModule;
035import ca.uhn.fhir.validation.ResultSeverityEnum;
036import ca.uhn.fhir.validation.SingleValidationMessage;
037import ca.uhn.fhir.validation.ValidationResult;
038import org.apache.commons.lang3.Validate;
039import org.apache.commons.lang3.text.StrLookup;
040import org.apache.commons.lang3.text.StrSubstitutor;
041import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045import java.util.ArrayList;
046import java.util.List;
047
048import static org.apache.commons.lang3.StringUtils.isNotBlank;
049
050/**
051 * This interceptor intercepts each incoming request and if it contains a FHIR resource, validates that resource. The
052 * interceptor may be configured to run any validator modules, and will then add headers to the response or fail the
053 * request with an {@link UnprocessableEntityException HTTP 422 Unprocessable Entity}.
054 */
055@Interceptor
056public abstract class BaseValidatingInterceptor<T> extends ValidationResultEnrichingInterceptor {
057
058        /**
059         * Default value:<br/>
060         * <code>
061         * ${row}:${col} ${severity} ${message} (${location})
062         * </code>
063         */
064        public static final String DEFAULT_RESPONSE_HEADER_VALUE = "${row}:${col} ${severity} ${message} (${location})";
065
066        private static final Logger ourLog = LoggerFactory.getLogger(BaseValidatingInterceptor.class);
067
068        private Integer myAddResponseIssueHeaderOnSeverity = null;
069        private Integer myAddResponseOutcomeHeaderOnSeverity = null;
070        private Integer myFailOnSeverity = ResultSeverityEnum.ERROR.ordinal();
071        private boolean myIgnoreValidatorExceptions;
072        private int myMaximumHeaderLength = 200;
073        private String myResponseIssueHeaderName = provideDefaultResponseHeaderName();
074        private String myResponseIssueHeaderValue = DEFAULT_RESPONSE_HEADER_VALUE;
075        private String myResponseIssueHeaderValueNoIssues = null;
076        private String myResponseOutcomeHeaderName = provideDefaultResponseHeaderName();
077
078        private List<IValidatorModule> myValidatorModules;
079        private FhirValidator myValidator;
080
081        private void addResponseIssueHeader(RequestDetails theRequestDetails, SingleValidationMessage theNext) {
082                // Perform any string substitutions from the message format
083                StrLookup<?> lookup = new MyLookup(theNext);
084                StrSubstitutor subs = new StrSubstitutor(lookup, "${", "}", '\\');
085
086                // Log the header
087                String headerValue = subs.replace(myResponseIssueHeaderValue);
088                ourLog.trace("Adding header to response: {}", headerValue);
089
090                theRequestDetails.getResponse().addHeader(myResponseIssueHeaderName, headerValue);
091        }
092
093        /**
094         * Specify a validator module to use.
095         *
096         * @see #setValidator(FhirValidator)
097         */
098        public BaseValidatingInterceptor<T> addValidatorModule(IValidatorModule theModule) {
099                Validate.notNull(theModule, "theModule must not be null");
100                Validate.isTrue(myValidator == null, "Can not specify both a validator and validator modules. Only one needs to be supplied.");
101                if (getValidatorModules() == null) {
102                        setValidatorModules(new ArrayList<>());
103                }
104                getValidatorModules().add(theModule);
105                return this;
106        }
107
108        /**
109         * Provides the validator to use. This can be used as an alternative to {@link #addValidatorModule(IValidatorModule)}
110         *
111         * @see #addValidatorModule(IValidatorModule)
112         * @see #setValidatorModules(List)
113         */
114        public void setValidator(FhirValidator theValidator) {
115                Validate.isTrue(theValidator == null || getValidatorModules() == null || getValidatorModules().isEmpty(), "Can not specify both a validator and validator modules. Only one needs to be supplied.");
116                myValidator = theValidator;
117        }
118
119
120        abstract ValidationResult doValidate(FhirValidator theValidator, T theRequest);
121
122        /**
123         * Fail the request by throwing an {@link UnprocessableEntityException} as a result of a validation failure.
124         * Subclasses may change this behaviour by providing alternate behaviour.
125         */
126        protected void fail(RequestDetails theRequestDetails, ValidationResult theValidationResult) {
127                throw new UnprocessableEntityException(Msg.code(330) + theValidationResult.getMessages().get(0).getMessage(), theValidationResult.toOperationOutcome());
128        }
129
130        /**
131         * If the validation produces a result with at least the given severity, a header with the name
132         * specified by {@link #setResponseOutcomeHeaderName(String)} will be added containing a JSON encoded
133         * OperationOutcome resource containing the validation results.
134         */
135        public ResultSeverityEnum getAddResponseOutcomeHeaderOnSeverity() {
136                return myAddResponseOutcomeHeaderOnSeverity != null ? ResultSeverityEnum.values()[myAddResponseOutcomeHeaderOnSeverity] : null;
137        }
138
139        /**
140         * If the validation produces a result with at least the given severity, a header with the name
141         * specified by {@link #setResponseOutcomeHeaderName(String)} will be added containing a JSON encoded
142         * OperationOutcome resource containing the validation results.
143         */
144        public void setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum theAddResponseOutcomeHeaderOnSeverity) {
145                myAddResponseOutcomeHeaderOnSeverity = theAddResponseOutcomeHeaderOnSeverity != null ? theAddResponseOutcomeHeaderOnSeverity.ordinal() : null;
146        }
147
148        /**
149         * The maximum length for an individual header. If an individual header would be written exceeding this length,
150         * the header value will be truncated.
151         */
152        public int getMaximumHeaderLength() {
153                return myMaximumHeaderLength;
154        }
155
156        /**
157         * The maximum length for an individual header. If an individual header would be written exceeding this length,
158         * the header value will be truncated. Value must be greater than 100.
159         */
160        public void setMaximumHeaderLength(int theMaximumHeaderLength) {
161                Validate.isTrue(theMaximumHeaderLength >= 100, "theMaximumHeadeerLength must be >= 100");
162                myMaximumHeaderLength = theMaximumHeaderLength;
163        }
164
165        /**
166         * The name of the header specified by {@link #setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum)}
167         */
168        public String getResponseOutcomeHeaderName() {
169                return myResponseOutcomeHeaderName;
170        }
171
172        /**
173         * The name of the header specified by {@link #setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum)}
174         */
175        public void setResponseOutcomeHeaderName(String theResponseOutcomeHeaderName) {
176                Validate.notEmpty(theResponseOutcomeHeaderName, "theResponseOutcomeHeaderName can not be empty or null");
177                myResponseOutcomeHeaderName = theResponseOutcomeHeaderName;
178        }
179
180        public List<IValidatorModule> getValidatorModules() {
181                return myValidatorModules;
182        }
183
184        public void setValidatorModules(List<IValidatorModule> theValidatorModules) {
185                Validate.isTrue(myValidator == null || theValidatorModules == null || theValidatorModules.isEmpty(), "Can not specify both a validator and validator modules. Only one needs to be supplied.");
186                myValidatorModules = theValidatorModules;
187        }
188
189        /**
190         * If set to <code>true</code> (default is <code>false</code>) this interceptor
191         * will exit immediately and allow processing to continue if the validator throws
192         * any exceptions.
193         * <p>
194         * This setting is mostly useful in testing situations
195         * </p>
196         */
197        public boolean isIgnoreValidatorExceptions() {
198                return myIgnoreValidatorExceptions;
199        }
200
201        /**
202         * If set to <code>true</code> (default is <code>false</code>) this interceptor
203         * will exit immediately and allow processing to continue if the validator throws
204         * any exceptions.
205         * <p>
206         * This setting is mostly useful in testing situations
207         * </p>
208         */
209        public void setIgnoreValidatorExceptions(boolean theIgnoreValidatorExceptions) {
210                myIgnoreValidatorExceptions = theIgnoreValidatorExceptions;
211        }
212
213        abstract String provideDefaultResponseHeaderName();
214
215        /**
216         * Sets the minimum severity at which an issue detected by the validator will result in a header being added to the
217         * response. Default is {@link ResultSeverityEnum#INFORMATION}. Set to <code>null</code> to disable this behaviour.
218         *
219         * @see #setResponseHeaderName(String)
220         * @see #setResponseHeaderValue(String)
221         */
222        public void setAddResponseHeaderOnSeverity(ResultSeverityEnum theSeverity) {
223                myAddResponseIssueHeaderOnSeverity = theSeverity != null ? theSeverity.ordinal() : null;
224        }
225
226        /**
227         * Sets the minimum severity at which an issue detected by the validator will fail/reject the request. Default is
228         * {@link ResultSeverityEnum#ERROR}. Set to <code>null</code> to disable this behaviour.
229         */
230        public void setFailOnSeverity(ResultSeverityEnum theSeverity) {
231                myFailOnSeverity = theSeverity != null ? theSeverity.ordinal() : null;
232        }
233
234        /**
235         * Sets the name of the response header to add validation failures to
236         *
237         * @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum)
238         */
239        protected void setResponseHeaderName(String theResponseHeaderName) {
240                Validate.notBlank(theResponseHeaderName, "theResponseHeaderName must not be blank or null");
241                myResponseIssueHeaderName = theResponseHeaderName;
242        }
243
244        /**
245         * Sets the value to add to the response header with the name specified by {@link #setResponseHeaderName(String)}
246         * when validation produces a message of severity equal to or greater than
247         * {@link #setAddResponseHeaderOnSeverity(ResultSeverityEnum)}
248         * <p>
249         * This field allows the following substitutions:
250         * </p>
251         * <table>
252         * <tr>
253         * <td>Name</td>
254         * <td>Value</td>
255         * </tr>
256         * <tr>
257         * <td>${line}</td>
258         * <td>The line in the request</td>
259         * </tr>
260         * <tr>
261         * <td>${col}</td>
262         * <td>The column in the request</td>
263         * </tr>
264         * <tr>
265         * <td>${location}</td>
266         * <td>The location in the payload as a string (typically this will be a path)</td>
267         * </tr>
268         * <tr>
269         * <td>${severity}</td>
270         * <td>The severity of the issue</td>
271         * </tr>
272         * <tr>
273         * <td>${message}</td>
274         * <td>The validation message</td>
275         * </tr>
276         * </table>
277         *
278         * @see #DEFAULT_RESPONSE_HEADER_VALUE
279         * @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum)
280         */
281        public void setResponseHeaderValue(String theResponseHeaderValue) {
282                Validate.notBlank(theResponseHeaderValue, "theResponseHeaderValue must not be blank or null");
283                myResponseIssueHeaderValue = theResponseHeaderValue;
284        }
285
286        /**
287         * Sets the header value to add when no issues are found at or exceeding the
288         * threshold specified by {@link #setAddResponseHeaderOnSeverity(ResultSeverityEnum)}
289         */
290        public void setResponseHeaderValueNoIssues(String theResponseHeaderValueNoIssues) {
291                myResponseIssueHeaderValueNoIssues = theResponseHeaderValueNoIssues;
292        }
293
294        /**
295         * Hook for subclasses (e.g. add a tag (coding) to an incoming resource when a given severity appears in the
296         * ValidationResult).
297         */
298        protected void postProcessResult(RequestDetails theRequestDetails, ValidationResult theValidationResult) {
299        }
300
301        /**
302         * Hook for subclasses on failure (e.g. add a response header to an incoming resource upon rejection).
303         */
304        protected void postProcessResultOnFailure(RequestDetails theRequestDetails, ValidationResult theValidationResult) {
305        }
306
307        /**
308         * Note: May return null
309         */
310        protected ValidationResult validate(T theRequest, RequestDetails theRequestDetails) {
311                if (theRequest == null || theRequestDetails == null) {
312                        return null;
313                }
314
315                RestOperationTypeEnum opType = theRequestDetails.getRestOperationType();
316                if (opType != null) {
317                        switch (opType) {
318                                case GRAPHQL_REQUEST:
319                                        return null;
320                                default:
321                                        break;
322                        }
323                }
324
325                FhirValidator validator;
326                if (myValidator != null) {
327                        validator = myValidator;
328                } else {
329                        validator = theRequestDetails.getServer().getFhirContext().newValidator();
330                        if (myValidatorModules != null) {
331                                for (IValidatorModule next : myValidatorModules) {
332                                        validator.registerValidatorModule(next);
333                                }
334                        }
335                }
336
337                ValidationResult validationResult;
338                try {
339                        validationResult = doValidate(validator, theRequest);
340                } catch (Exception e) {
341                        if (myIgnoreValidatorExceptions) {
342                                ourLog.warn("Validator threw an exception during validation", e);
343                                return null;
344                        }
345                        if (e instanceof BaseServerResponseException) {
346                                throw (BaseServerResponseException) e;
347                        }
348                        throw new InternalErrorException(Msg.code(331) + e);
349                }
350
351                if (myAddResponseIssueHeaderOnSeverity != null) {
352                        boolean found = false;
353                        for (SingleValidationMessage next : validationResult.getMessages()) {
354                                if (next.getSeverity().ordinal() >= myAddResponseIssueHeaderOnSeverity) {
355                                        addResponseIssueHeader(theRequestDetails, next);
356                                        found = true;
357                                }
358                        }
359                        if (!found) {
360                                if (isNotBlank(myResponseIssueHeaderValueNoIssues)) {
361                                        theRequestDetails.getResponse().addHeader(myResponseIssueHeaderName, myResponseIssueHeaderValueNoIssues);
362                                }
363                        }
364                }
365
366                if (myFailOnSeverity != null) {
367                        for (SingleValidationMessage next : validationResult.getMessages()) {
368                                if (next.getSeverity().ordinal() >= myFailOnSeverity) {
369                                        postProcessResultOnFailure(theRequestDetails, validationResult);
370                                        fail(theRequestDetails, validationResult);
371                                        return validationResult;
372                                }
373                        }
374                }
375
376                if (myAddResponseOutcomeHeaderOnSeverity != null) {
377                        IBaseOperationOutcome outcome = null;
378                        for (SingleValidationMessage next : validationResult.getMessages()) {
379                                if (next.getSeverity().ordinal() >= myAddResponseOutcomeHeaderOnSeverity) {
380                                        outcome = validationResult.toOperationOutcome();
381                                        break;
382                                }
383                        }
384                        if (outcome == null && myAddResponseOutcomeHeaderOnSeverity != null && myAddResponseOutcomeHeaderOnSeverity == ResultSeverityEnum.INFORMATION.ordinal()) {
385                                FhirContext ctx = theRequestDetails.getServer().getFhirContext();
386                                outcome = OperationOutcomeUtil.newInstance(ctx);
387                                OperationOutcomeUtil.addIssue(ctx, outcome, "information", "No issues detected", "", "informational");
388                        }
389
390                        if (outcome != null) {
391                                IParser parser = theRequestDetails.getServer().getFhirContext().newJsonParser().setPrettyPrint(false);
392                                String encoded = parser.encodeResourceToString(outcome);
393                                if (encoded.length() > getMaximumHeaderLength()) {
394                                        encoded = encoded.substring(0, getMaximumHeaderLength() - 3) + "...";
395                                }
396                                theRequestDetails.getResponse().addHeader(myResponseOutcomeHeaderName, encoded);
397                        }
398                }
399
400                postProcessResult(theRequestDetails, validationResult);
401
402                return validationResult;
403        }
404
405        private static class MyLookup extends StrLookup<String> {
406
407                private SingleValidationMessage myMessage;
408
409                public MyLookup(SingleValidationMessage theMessage) {
410                        myMessage = theMessage;
411                }
412
413                @Override
414                public String lookup(String theKey) {
415                        if ("line".equals(theKey)) {
416                                return toString(myMessage.getLocationLine());
417                        }
418                        if ("col".equals(theKey)) {
419                                return toString(myMessage.getLocationCol());
420                        }
421                        if ("message".equals(theKey)) {
422                                return toString(myMessage.getMessage());
423                        }
424                        if ("location".equals(theKey)) {
425                                return toString(myMessage.getLocationString());
426                        }
427                        if ("severity".equals(theKey)) {
428                                return myMessage.getSeverity() != null ? myMessage.getSeverity().name() : null;
429                        }
430
431                        return "";
432                }
433
434                private static String toString(Object theInt) {
435                        return theInt != null ? theInt.toString() : "";
436                }
437
438        }
439
440}