001package ca.uhn.fhir.rest.server.interceptor;
002
003import static org.apache.commons.lang3.StringUtils.isBlank;
004
005/*
006 * #%L
007 * HAPI FHIR - Server Framework
008 * %%
009 * Copyright (C) 2014 - 2019 University Health Network
010 * %%
011 * Licensed under the Apache License, Version 2.0 (the "License");
012 * you may not use this file except in compliance with the License.
013 * You may obtain a copy of the License at
014 *
015 * http://www.apache.org/licenses/LICENSE-2.0
016 *
017 * Unless required by applicable law or agreed to in writing, software
018 * distributed under the License is distributed on an "AS IS" BASIS,
019 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
020 * See the License for the specific language governing permissions and
021 * limitations under the License.
022 * #L%
023 */
024
025import java.nio.charset.Charset;
026
027import javax.servlet.http.HttpServletRequest;
028import javax.servlet.http.HttpServletResponse;
029
030import ca.uhn.fhir.interceptor.api.Hook;
031import ca.uhn.fhir.interceptor.api.Pointcut;
032import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
033import org.hl7.fhir.instance.model.api.IBaseResource;
034
035import ca.uhn.fhir.rest.api.EncodingEnum;
036import ca.uhn.fhir.rest.api.server.RequestDetails;
037import ca.uhn.fhir.rest.server.RestfulServerUtils;
038import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
039import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
040import ca.uhn.fhir.rest.server.method.ResourceParameter;
041import ca.uhn.fhir.validation.FhirValidator;
042import ca.uhn.fhir.validation.ResultSeverityEnum;
043import ca.uhn.fhir.validation.ValidationResult;
044
045/**
046 * This interceptor intercepts each incoming request and if it contains a FHIR resource, validates that resource. The
047 * interceptor may be configured to run any validator modules, and will then add headers to the response or fail the
048 * request with an {@link UnprocessableEntityException HTTP 422 Unprocessable Entity}.
049 */
050public class RequestValidatingInterceptor extends BaseValidatingInterceptor<String> {
051
052        /**
053         * X-HAPI-Request-Validation
054         */
055        public static final String DEFAULT_RESPONSE_HEADER_NAME = "X-FHIR-Request-Validation";
056
057        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RequestValidatingInterceptor.class);
058
059        /**
060         * A {@link RequestDetails#getUserData() user data} entry will be created with this
061         * key which contains the {@link ValidationResult} from validating the request.
062         */
063        public static final String REQUEST_VALIDATION_RESULT = RequestValidatingInterceptor.class.getName() + "_REQUEST_VALIDATION_RESULT";
064
065        private boolean myAddValidationResultsToResponseOperationOutcome = true;
066
067        @Override
068        ValidationResult doValidate(FhirValidator theValidator, String theRequest) {
069                return theValidator.validateWithResult(theRequest);
070        }
071
072        @Hook(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED)
073        public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException {
074                EncodingEnum encoding = RestfulServerUtils.determineRequestEncodingNoDefault(theRequestDetails);
075                if (encoding == null) {
076                        ourLog.trace("Incoming request does not appear to be FHIR, not going to validate");
077                        return true;
078                }
079
080                Charset charset = ResourceParameter.determineRequestCharset(theRequestDetails);
081                String requestText = new String(theRequestDetails.loadRequestContents(), charset);
082
083                if (isBlank(requestText)) {
084                        ourLog.trace("Incoming request does not have a body");
085                        return true;
086                }
087
088                ValidationResult validationResult = validate(requestText, theRequestDetails);
089
090                // The JPA server will use this
091                theRequestDetails.getUserData().put(REQUEST_VALIDATION_RESULT, validationResult);
092
093                return true;
094        }
095
096        /**
097         * If set to {@literal true} (default is true), the validation results
098         * will be added to the OperationOutcome being returned to the client,
099         * unless the response being returned is not an OperationOutcome
100         * to begin with (e.g. if the client has requested 
101         * <code>Return: prefer=representation</code>)
102         */
103        public boolean isAddValidationResultsToResponseOperationOutcome() {
104                return myAddValidationResultsToResponseOperationOutcome;
105        }
106
107        @Hook(Pointcut.SERVER_OUTGOING_RESPONSE)
108        public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) {
109                if (myAddValidationResultsToResponseOperationOutcome) {
110                        if (theResponseObject instanceof IBaseOperationOutcome) {
111                                IBaseOperationOutcome oo = (IBaseOperationOutcome) theResponseObject;
112
113                                if (theRequestDetails != null) {
114                                        ValidationResult validationResult = (ValidationResult) theRequestDetails.getUserData().get(RequestValidatingInterceptor.REQUEST_VALIDATION_RESULT);
115                                        if (validationResult != null) {
116                                                validationResult.populateOperationOutcome(oo);
117                                        }
118                                }
119
120                        }
121                }
122
123                return true;
124        }
125
126        @Override
127        String provideDefaultResponseHeaderName() {
128                return DEFAULT_RESPONSE_HEADER_NAME;
129        }
130
131        /**
132         * If set to {@literal true} (default is true), the validation results
133         * will be added to the OperationOutcome being returned to the client,
134         * unless the response being returned is not an OperationOutcome
135         * to begin with (e.g. if the client has requested 
136         * <code>Return: prefer=representation</code>)
137         */
138        public void setAddValidationResultsToResponseOperationOutcome(boolean theAddValidationResultsToResponseOperationOutcome) {
139                myAddValidationResultsToResponseOperationOutcome = theAddValidationResultsToResponseOperationOutcome;
140        }
141
142        /**
143         * Sets the name of the response header to add validation failures to
144         * 
145         * @see #DEFAULT_RESPONSE_HEADER_NAME
146         * @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum)
147         */
148        @Override
149        public void setResponseHeaderName(String theResponseHeaderName) {
150                super.setResponseHeaderName(theResponseHeaderName);
151        }
152
153}