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