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