001/*
002 * #%L
003 * HAPI FHIR - Server Framework
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.rest.server.interceptor;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.interceptor.api.Interceptor;
025import ca.uhn.fhir.parser.IParser;
026import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
027import ca.uhn.fhir.rest.api.server.RequestDetails;
028import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
029import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
030import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
031import ca.uhn.fhir.util.OperationOutcomeUtil;
032import ca.uhn.fhir.validation.FhirValidator;
033import ca.uhn.fhir.validation.IValidatorModule;
034import ca.uhn.fhir.validation.ResultSeverityEnum;
035import ca.uhn.fhir.validation.SingleValidationMessage;
036import ca.uhn.fhir.validation.ValidationOptions;
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(
101                                myValidator == null,
102                                "Can not specify both a validator and validator modules. Only one needs to be supplied.");
103                if (getValidatorModules() == null) {
104                        setValidatorModules(new ArrayList<>());
105                }
106                getValidatorModules().add(theModule);
107                return this;
108        }
109
110        /**
111         * Provides the validator to use. This can be used as an alternative to {@link #addValidatorModule(IValidatorModule)}
112         *
113         * @see #addValidatorModule(IValidatorModule)
114         * @see #setValidatorModules(List)
115         */
116        public void setValidator(FhirValidator theValidator) {
117                Validate.isTrue(
118                                theValidator == null
119                                                || getValidatorModules() == null
120                                                || getValidatorModules().isEmpty(),
121                                "Can not specify both a validator and validator modules. Only one needs to be supplied.");
122                myValidator = theValidator;
123        }
124
125        @Deprecated
126        public ValidationResult doValidate(FhirValidator theValidator, T theRequest) {
127                return doValidate(theValidator, theRequest, ValidationOptions.empty());
128        }
129
130        abstract ValidationResult doValidate(FhirValidator theValidator, T theRequest, ValidationOptions theOptions);
131
132        /**
133         * Fail the request by throwing an {@link UnprocessableEntityException} as a result of a validation failure.
134         * Subclasses may change this behaviour by providing alternate behaviour.
135         */
136        protected void fail(RequestDetails theRequestDetails, ValidationResult theValidationResult) {
137                throw new UnprocessableEntityException(
138                                Msg.code(330) + theValidationResult.getMessages().get(0).getMessage(),
139                                theValidationResult.toOperationOutcome());
140        }
141
142        /**
143         * If the validation produces a result with at least the given severity, a header with the name
144         * specified by {@link #setResponseOutcomeHeaderName(String)} will be added containing a JSON encoded
145         * OperationOutcome resource containing the validation results.
146         */
147        public ResultSeverityEnum getAddResponseOutcomeHeaderOnSeverity() {
148                return myAddResponseOutcomeHeaderOnSeverity != null
149                                ? ResultSeverityEnum.values()[myAddResponseOutcomeHeaderOnSeverity]
150                                : null;
151        }
152
153        /**
154         * If the validation produces a result with at least the given severity, a header with the name
155         * specified by {@link #setResponseOutcomeHeaderName(String)} will be added containing a JSON encoded
156         * OperationOutcome resource containing the validation results.
157         */
158        public void setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum theAddResponseOutcomeHeaderOnSeverity) {
159                myAddResponseOutcomeHeaderOnSeverity =
160                                theAddResponseOutcomeHeaderOnSeverity != null ? theAddResponseOutcomeHeaderOnSeverity.ordinal() : null;
161        }
162
163        /**
164         * The maximum length for an individual header. If an individual header would be written exceeding this length,
165         * the header value will be truncated.
166         */
167        public int getMaximumHeaderLength() {
168                return myMaximumHeaderLength;
169        }
170
171        /**
172         * The maximum length for an individual header. If an individual header would be written exceeding this length,
173         * the header value will be truncated. Value must be greater than 100.
174         */
175        public void setMaximumHeaderLength(int theMaximumHeaderLength) {
176                Validate.isTrue(theMaximumHeaderLength >= 100, "theMaximumHeadeerLength must be >= 100");
177                myMaximumHeaderLength = theMaximumHeaderLength;
178        }
179
180        /**
181         * The name of the header specified by {@link #setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum)}
182         */
183        public String getResponseOutcomeHeaderName() {
184                return myResponseOutcomeHeaderName;
185        }
186
187        /**
188         * The name of the header specified by {@link #setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum)}
189         */
190        public void setResponseOutcomeHeaderName(String theResponseOutcomeHeaderName) {
191                Validate.notEmpty(theResponseOutcomeHeaderName, "theResponseOutcomeHeaderName can not be empty or null");
192                myResponseOutcomeHeaderName = theResponseOutcomeHeaderName;
193        }
194
195        public List<IValidatorModule> getValidatorModules() {
196                return myValidatorModules;
197        }
198
199        public void setValidatorModules(List<IValidatorModule> theValidatorModules) {
200                Validate.isTrue(
201                                myValidator == null || theValidatorModules == null || theValidatorModules.isEmpty(),
202                                "Can not specify both a validator and validator modules. Only one needs to be supplied.");
203                myValidatorModules = theValidatorModules;
204        }
205
206        /**
207         * If set to <code>true</code> (default is <code>false</code>) this interceptor
208         * will exit immediately and allow processing to continue if the validator throws
209         * any exceptions.
210         * <p>
211         * This setting is mostly useful in testing situations
212         * </p>
213         */
214        public boolean isIgnoreValidatorExceptions() {
215                return myIgnoreValidatorExceptions;
216        }
217
218        /**
219         * If set to <code>true</code> (default is <code>false</code>) this interceptor
220         * will exit immediately and allow processing to continue if the validator throws
221         * any exceptions.
222         * <p>
223         * This setting is mostly useful in testing situations
224         * </p>
225         */
226        public void setIgnoreValidatorExceptions(boolean theIgnoreValidatorExceptions) {
227                myIgnoreValidatorExceptions = theIgnoreValidatorExceptions;
228        }
229
230        abstract String provideDefaultResponseHeaderName();
231
232        /**
233         * Sets the minimum severity at which an issue detected by the validator will result in a header being added to the
234         * response. Default is {@link ResultSeverityEnum#INFORMATION}. Set to <code>null</code> to disable this behaviour.
235         *
236         * @see #setResponseHeaderName(String)
237         * @see #setResponseHeaderValue(String)
238         */
239        public void setAddResponseHeaderOnSeverity(ResultSeverityEnum theSeverity) {
240                myAddResponseIssueHeaderOnSeverity = theSeverity != null ? theSeverity.ordinal() : null;
241        }
242
243        /**
244         * Sets the minimum severity at which an issue detected by the validator will fail/reject the request. Default is
245         * {@link ResultSeverityEnum#ERROR}. Set to <code>null</code> to disable this behaviour.
246         */
247        public void setFailOnSeverity(ResultSeverityEnum theSeverity) {
248                myFailOnSeverity = theSeverity != null ? theSeverity.ordinal() : null;
249        }
250
251        /**
252         * Sets the name of the response header to add validation failures to
253         *
254         * @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum)
255         */
256        protected void setResponseHeaderName(String theResponseHeaderName) {
257                Validate.notBlank(theResponseHeaderName, "theResponseHeaderName must not be blank or null");
258                myResponseIssueHeaderName = theResponseHeaderName;
259        }
260
261        /**
262         * Sets the value to add to the response header with the name specified by {@link #setResponseHeaderName(String)}
263         * when validation produces a message of severity equal to or greater than
264         * {@link #setAddResponseHeaderOnSeverity(ResultSeverityEnum)}
265         * <p>
266         * This field allows the following substitutions:
267         * </p>
268         * <table>
269         * <tr>
270         * <td>Name</td>
271         * <td>Value</td>
272         * </tr>
273         * <tr>
274         * <td>${line}</td>
275         * <td>The line in the request</td>
276         * </tr>
277         * <tr>
278         * <td>${col}</td>
279         * <td>The column in the request</td>
280         * </tr>
281         * <tr>
282         * <td>${location}</td>
283         * <td>The location in the payload as a string (typically this will be a path)</td>
284         * </tr>
285         * <tr>
286         * <td>${severity}</td>
287         * <td>The severity of the issue</td>
288         * </tr>
289         * <tr>
290         * <td>${message}</td>
291         * <td>The validation message</td>
292         * </tr>
293         * </table>
294         *
295         * @see #DEFAULT_RESPONSE_HEADER_VALUE
296         * @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum)
297         */
298        public void setResponseHeaderValue(String theResponseHeaderValue) {
299                Validate.notBlank(theResponseHeaderValue, "theResponseHeaderValue must not be blank or null");
300                myResponseIssueHeaderValue = theResponseHeaderValue;
301        }
302
303        /**
304         * Sets the header value to add when no issues are found at or exceeding the
305         * threshold specified by {@link #setAddResponseHeaderOnSeverity(ResultSeverityEnum)}
306         */
307        public void setResponseHeaderValueNoIssues(String theResponseHeaderValueNoIssues) {
308                myResponseIssueHeaderValueNoIssues = theResponseHeaderValueNoIssues;
309        }
310
311        /**
312         * Hook for subclasses (e.g. add a tag (coding) to an incoming resource when a given severity appears in the
313         * ValidationResult).
314         */
315        protected void postProcessResult(RequestDetails theRequestDetails, ValidationResult theValidationResult) {}
316
317        /**
318         * Hook for subclasses on failure (e.g. add a response header to an incoming resource upon rejection).
319         */
320        protected void postProcessResultOnFailure(RequestDetails theRequestDetails, ValidationResult theValidationResult) {}
321
322        /**
323         * Note: May return null
324         */
325        protected ValidationResult validate(T theRequest, RequestDetails theRequestDetails) {
326                if (theRequest == null || theRequestDetails == null) {
327                        return null;
328                }
329
330                RestOperationTypeEnum opType = theRequestDetails.getRestOperationType();
331                if (opType != null) {
332                        switch (opType) {
333                                case GRAPHQL_REQUEST:
334                                        return null;
335                                default:
336                                        break;
337                        }
338                }
339
340                FhirValidator validator;
341                if (myValidator != null) {
342                        validator = myValidator;
343                } else {
344                        validator = theRequestDetails.getServer().getFhirContext().newValidator();
345                        if (myValidatorModules != null) {
346                                for (IValidatorModule next : myValidatorModules) {
347                                        validator.registerValidatorModule(next);
348                                }
349                        }
350                }
351
352                ValidationResult validationResult;
353                try {
354                        ValidationOptions options = new ValidationOptions();
355                        options.setAppContext(theRequestDetails);
356                        validationResult = doValidate(validator, theRequest, options);
357                } catch (Exception e) {
358                        if (myIgnoreValidatorExceptions) {
359                                ourLog.warn("Validator threw an exception during validation", e);
360                                return null;
361                        }
362                        if (e instanceof BaseServerResponseException) {
363                                throw (BaseServerResponseException) e;
364                        }
365                        throw new InternalErrorException(Msg.code(331) + e);
366                }
367
368                if (myAddResponseIssueHeaderOnSeverity != null) {
369                        boolean found = false;
370                        for (SingleValidationMessage next : validationResult.getMessages()) {
371                                if (next.getSeverity().ordinal() >= myAddResponseIssueHeaderOnSeverity) {
372                                        addResponseIssueHeader(theRequestDetails, next);
373                                        found = true;
374                                }
375                        }
376                        if (!found) {
377                                if (isNotBlank(myResponseIssueHeaderValueNoIssues)) {
378                                        theRequestDetails
379                                                        .getResponse()
380                                                        .addHeader(myResponseIssueHeaderName, myResponseIssueHeaderValueNoIssues);
381                                }
382                        }
383                }
384
385                if (myFailOnSeverity != null) {
386                        for (SingleValidationMessage next : validationResult.getMessages()) {
387                                if (next.getSeverity().ordinal() >= myFailOnSeverity) {
388                                        postProcessResultOnFailure(theRequestDetails, validationResult);
389                                        fail(theRequestDetails, validationResult);
390                                        return validationResult;
391                                }
392                        }
393                }
394
395                if (myAddResponseOutcomeHeaderOnSeverity != null) {
396                        IBaseOperationOutcome outcome = null;
397                        for (SingleValidationMessage next : validationResult.getMessages()) {
398                                if (next.getSeverity().ordinal() >= myAddResponseOutcomeHeaderOnSeverity) {
399                                        outcome = validationResult.toOperationOutcome();
400                                        break;
401                                }
402                        }
403                        if (outcome == null
404                                        && myAddResponseOutcomeHeaderOnSeverity != null
405                                        && myAddResponseOutcomeHeaderOnSeverity == ResultSeverityEnum.INFORMATION.ordinal()) {
406                                FhirContext ctx = theRequestDetails.getServer().getFhirContext();
407                                outcome = OperationOutcomeUtil.newInstance(ctx);
408                                OperationOutcomeUtil.addIssue(ctx, outcome, "information", "No issues detected", "", "informational");
409                        }
410
411                        if (outcome != null) {
412                                IParser parser = theRequestDetails
413                                                .getServer()
414                                                .getFhirContext()
415                                                .newJsonParser()
416                                                .setPrettyPrint(false);
417                                String encoded = parser.encodeResourceToString(outcome);
418                                if (encoded.length() > getMaximumHeaderLength()) {
419                                        encoded = encoded.substring(0, getMaximumHeaderLength() - 3) + "...";
420                                }
421                                theRequestDetails.getResponse().addHeader(myResponseOutcomeHeaderName, encoded);
422                        }
423                }
424
425                postProcessResult(theRequestDetails, validationResult);
426
427                return validationResult;
428        }
429
430        private static class MyLookup extends StrLookup<String> {
431
432                private SingleValidationMessage myMessage;
433
434                public MyLookup(SingleValidationMessage theMessage) {
435                        myMessage = theMessage;
436                }
437
438                @Override
439                public String lookup(String theKey) {
440                        if ("line".equals(theKey)) {
441                                return toString(myMessage.getLocationLine());
442                        }
443                        if ("col".equals(theKey)) {
444                                return toString(myMessage.getLocationCol());
445                        }
446                        if ("message".equals(theKey)) {
447                                return toString(myMessage.getMessage());
448                        }
449                        if ("location".equals(theKey)) {
450                                return toString(myMessage.getLocationString());
451                        }
452                        if ("severity".equals(theKey)) {
453                                return myMessage.getSeverity() != null ? myMessage.getSeverity().name() : null;
454                        }
455
456                        return "";
457                }
458
459                private static String toString(Object theInt) {
460                        return theInt != null ? theInt.toString() : "";
461                }
462        }
463}