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