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}