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