
001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.server.interceptor; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.Interceptor; 025import ca.uhn.fhir.parser.IParser; 026import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 027import ca.uhn.fhir.rest.api.server.RequestDetails; 028import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 029import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 030import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 031import ca.uhn.fhir.util.OperationOutcomeUtil; 032import ca.uhn.fhir.validation.FhirValidator; 033import ca.uhn.fhir.validation.IValidatorModule; 034import ca.uhn.fhir.validation.ResultSeverityEnum; 035import ca.uhn.fhir.validation.SingleValidationMessage; 036import ca.uhn.fhir.validation.ValidationOptions; 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( 101 myValidator == null, 102 "Can not specify both a validator and validator modules. Only one needs to be supplied."); 103 if (getValidatorModules() == null) { 104 setValidatorModules(new ArrayList<>()); 105 } 106 getValidatorModules().add(theModule); 107 return this; 108 } 109 110 /** 111 * Provides the validator to use. This can be used as an alternative to {@link #addValidatorModule(IValidatorModule)} 112 * 113 * @see #addValidatorModule(IValidatorModule) 114 * @see #setValidatorModules(List) 115 */ 116 public void setValidator(FhirValidator theValidator) { 117 Validate.isTrue( 118 theValidator == null 119 || getValidatorModules() == null 120 || getValidatorModules().isEmpty(), 121 "Can not specify both a validator and validator modules. Only one needs to be supplied."); 122 myValidator = theValidator; 123 } 124 125 @Deprecated 126 public ValidationResult doValidate(FhirValidator theValidator, T theRequest) { 127 return doValidate(theValidator, theRequest, ValidationOptions.empty()); 128 } 129 130 abstract ValidationResult doValidate(FhirValidator theValidator, T theRequest, ValidationOptions theOptions); 131 132 /** 133 * Fail the request by throwing an {@link UnprocessableEntityException} as a result of a validation failure. 134 * Subclasses may change this behaviour by providing alternate behaviour. 135 */ 136 protected void fail(RequestDetails theRequestDetails, ValidationResult theValidationResult) { 137 throw new UnprocessableEntityException( 138 Msg.code(330) + theValidationResult.getMessages().get(0).getMessage(), 139 theValidationResult.toOperationOutcome()); 140 } 141 142 /** 143 * If the validation produces a result with at least the given severity, a header with the name 144 * specified by {@link #setResponseOutcomeHeaderName(String)} will be added containing a JSON encoded 145 * OperationOutcome resource containing the validation results. 146 */ 147 public ResultSeverityEnum getAddResponseOutcomeHeaderOnSeverity() { 148 return myAddResponseOutcomeHeaderOnSeverity != null 149 ? ResultSeverityEnum.values()[myAddResponseOutcomeHeaderOnSeverity] 150 : null; 151 } 152 153 /** 154 * If the validation produces a result with at least the given severity, a header with the name 155 * specified by {@link #setResponseOutcomeHeaderName(String)} will be added containing a JSON encoded 156 * OperationOutcome resource containing the validation results. 157 */ 158 public void setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum theAddResponseOutcomeHeaderOnSeverity) { 159 myAddResponseOutcomeHeaderOnSeverity = 160 theAddResponseOutcomeHeaderOnSeverity != null ? theAddResponseOutcomeHeaderOnSeverity.ordinal() : null; 161 } 162 163 /** 164 * The maximum length for an individual header. If an individual header would be written exceeding this length, 165 * the header value will be truncated. 166 */ 167 public int getMaximumHeaderLength() { 168 return myMaximumHeaderLength; 169 } 170 171 /** 172 * The maximum length for an individual header. If an individual header would be written exceeding this length, 173 * the header value will be truncated. Value must be greater than 100. 174 */ 175 public void setMaximumHeaderLength(int theMaximumHeaderLength) { 176 Validate.isTrue(theMaximumHeaderLength >= 100, "theMaximumHeadeerLength must be >= 100"); 177 myMaximumHeaderLength = theMaximumHeaderLength; 178 } 179 180 /** 181 * The name of the header specified by {@link #setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum)} 182 */ 183 public String getResponseOutcomeHeaderName() { 184 return myResponseOutcomeHeaderName; 185 } 186 187 /** 188 * The name of the header specified by {@link #setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum)} 189 */ 190 public void setResponseOutcomeHeaderName(String theResponseOutcomeHeaderName) { 191 Validate.notEmpty(theResponseOutcomeHeaderName, "theResponseOutcomeHeaderName can not be empty or null"); 192 myResponseOutcomeHeaderName = theResponseOutcomeHeaderName; 193 } 194 195 public List<IValidatorModule> getValidatorModules() { 196 return myValidatorModules; 197 } 198 199 public void setValidatorModules(List<IValidatorModule> theValidatorModules) { 200 Validate.isTrue( 201 myValidator == null || theValidatorModules == null || theValidatorModules.isEmpty(), 202 "Can not specify both a validator and validator modules. Only one needs to be supplied."); 203 myValidatorModules = theValidatorModules; 204 } 205 206 /** 207 * If set to <code>true</code> (default is <code>false</code>) this interceptor 208 * will exit immediately and allow processing to continue if the validator throws 209 * any exceptions. 210 * <p> 211 * This setting is mostly useful in testing situations 212 * </p> 213 */ 214 public boolean isIgnoreValidatorExceptions() { 215 return myIgnoreValidatorExceptions; 216 } 217 218 /** 219 * If set to <code>true</code> (default is <code>false</code>) this interceptor 220 * will exit immediately and allow processing to continue if the validator throws 221 * any exceptions. 222 * <p> 223 * This setting is mostly useful in testing situations 224 * </p> 225 */ 226 public void setIgnoreValidatorExceptions(boolean theIgnoreValidatorExceptions) { 227 myIgnoreValidatorExceptions = theIgnoreValidatorExceptions; 228 } 229 230 abstract String provideDefaultResponseHeaderName(); 231 232 /** 233 * Sets the minimum severity at which an issue detected by the validator will result in a header being added to the 234 * response. Default is {@link ResultSeverityEnum#INFORMATION}. Set to <code>null</code> to disable this behaviour. 235 * 236 * @see #setResponseHeaderName(String) 237 * @see #setResponseHeaderValue(String) 238 */ 239 public void setAddResponseHeaderOnSeverity(ResultSeverityEnum theSeverity) { 240 myAddResponseIssueHeaderOnSeverity = theSeverity != null ? theSeverity.ordinal() : null; 241 } 242 243 /** 244 * Sets the minimum severity at which an issue detected by the validator will fail/reject the request. Default is 245 * {@link ResultSeverityEnum#ERROR}. Set to <code>null</code> to disable this behaviour. 246 */ 247 public void setFailOnSeverity(ResultSeverityEnum theSeverity) { 248 myFailOnSeverity = theSeverity != null ? theSeverity.ordinal() : null; 249 } 250 251 /** 252 * Sets the name of the response header to add validation failures to 253 * 254 * @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum) 255 */ 256 protected void setResponseHeaderName(String theResponseHeaderName) { 257 Validate.notBlank(theResponseHeaderName, "theResponseHeaderName must not be blank or null"); 258 myResponseIssueHeaderName = theResponseHeaderName; 259 } 260 261 /** 262 * Sets the value to add to the response header with the name specified by {@link #setResponseHeaderName(String)} 263 * when validation produces a message of severity equal to or greater than 264 * {@link #setAddResponseHeaderOnSeverity(ResultSeverityEnum)} 265 * <p> 266 * This field allows the following substitutions: 267 * </p> 268 * <table> 269 * <tr> 270 * <td>Name</td> 271 * <td>Value</td> 272 * </tr> 273 * <tr> 274 * <td>${line}</td> 275 * <td>The line in the request</td> 276 * </tr> 277 * <tr> 278 * <td>${col}</td> 279 * <td>The column in the request</td> 280 * </tr> 281 * <tr> 282 * <td>${location}</td> 283 * <td>The location in the payload as a string (typically this will be a path)</td> 284 * </tr> 285 * <tr> 286 * <td>${severity}</td> 287 * <td>The severity of the issue</td> 288 * </tr> 289 * <tr> 290 * <td>${message}</td> 291 * <td>The validation message</td> 292 * </tr> 293 * </table> 294 * 295 * @see #DEFAULT_RESPONSE_HEADER_VALUE 296 * @see #setAddResponseHeaderOnSeverity(ResultSeverityEnum) 297 */ 298 public void setResponseHeaderValue(String theResponseHeaderValue) { 299 Validate.notBlank(theResponseHeaderValue, "theResponseHeaderValue must not be blank or null"); 300 myResponseIssueHeaderValue = theResponseHeaderValue; 301 } 302 303 /** 304 * Sets the header value to add when no issues are found at or exceeding the 305 * threshold specified by {@link #setAddResponseHeaderOnSeverity(ResultSeverityEnum)} 306 */ 307 public void setResponseHeaderValueNoIssues(String theResponseHeaderValueNoIssues) { 308 myResponseIssueHeaderValueNoIssues = theResponseHeaderValueNoIssues; 309 } 310 311 /** 312 * Hook for subclasses (e.g. add a tag (coding) to an incoming resource when a given severity appears in the 313 * ValidationResult). 314 */ 315 protected void postProcessResult(RequestDetails theRequestDetails, ValidationResult theValidationResult) {} 316 317 /** 318 * Hook for subclasses on failure (e.g. add a response header to an incoming resource upon rejection). 319 */ 320 protected void postProcessResultOnFailure(RequestDetails theRequestDetails, ValidationResult theValidationResult) {} 321 322 /** 323 * Note: May return null 324 */ 325 protected ValidationResult validate(T theRequest, RequestDetails theRequestDetails) { 326 if (theRequest == null || theRequestDetails == null) { 327 return null; 328 } 329 330 RestOperationTypeEnum opType = theRequestDetails.getRestOperationType(); 331 if (opType != null) { 332 switch (opType) { 333 case GRAPHQL_REQUEST: 334 return null; 335 default: 336 break; 337 } 338 } 339 340 FhirValidator validator; 341 if (myValidator != null) { 342 validator = myValidator; 343 } else { 344 validator = theRequestDetails.getServer().getFhirContext().newValidator(); 345 if (myValidatorModules != null) { 346 for (IValidatorModule next : myValidatorModules) { 347 validator.registerValidatorModule(next); 348 } 349 } 350 } 351 352 ValidationResult validationResult; 353 try { 354 ValidationOptions options = new ValidationOptions(); 355 options.setAppContext(theRequestDetails); 356 validationResult = doValidate(validator, theRequest, options); 357 } catch (Exception e) { 358 if (myIgnoreValidatorExceptions) { 359 ourLog.warn("Validator threw an exception during validation", e); 360 return null; 361 } 362 if (e instanceof BaseServerResponseException) { 363 throw (BaseServerResponseException) e; 364 } 365 throw new InternalErrorException(Msg.code(331) + e); 366 } 367 368 if (myAddResponseIssueHeaderOnSeverity != null) { 369 boolean found = false; 370 for (SingleValidationMessage next : validationResult.getMessages()) { 371 if (next.getSeverity().ordinal() >= myAddResponseIssueHeaderOnSeverity) { 372 addResponseIssueHeader(theRequestDetails, next); 373 found = true; 374 } 375 } 376 if (!found) { 377 if (isNotBlank(myResponseIssueHeaderValueNoIssues)) { 378 theRequestDetails 379 .getResponse() 380 .addHeader(myResponseIssueHeaderName, myResponseIssueHeaderValueNoIssues); 381 } 382 } 383 } 384 385 if (myFailOnSeverity != null) { 386 for (SingleValidationMessage next : validationResult.getMessages()) { 387 if (next.getSeverity().ordinal() >= myFailOnSeverity) { 388 postProcessResultOnFailure(theRequestDetails, validationResult); 389 fail(theRequestDetails, validationResult); 390 return validationResult; 391 } 392 } 393 } 394 395 if (myAddResponseOutcomeHeaderOnSeverity != null) { 396 IBaseOperationOutcome outcome = null; 397 for (SingleValidationMessage next : validationResult.getMessages()) { 398 if (next.getSeverity().ordinal() >= myAddResponseOutcomeHeaderOnSeverity) { 399 outcome = validationResult.toOperationOutcome(); 400 break; 401 } 402 } 403 if (outcome == null 404 && myAddResponseOutcomeHeaderOnSeverity != null 405 && myAddResponseOutcomeHeaderOnSeverity == ResultSeverityEnum.INFORMATION.ordinal()) { 406 FhirContext ctx = theRequestDetails.getServer().getFhirContext(); 407 outcome = OperationOutcomeUtil.newInstance(ctx); 408 OperationOutcomeUtil.addIssue(ctx, outcome, "information", "No issues detected", "", "informational"); 409 } 410 411 if (outcome != null) { 412 IParser parser = theRequestDetails 413 .getServer() 414 .getFhirContext() 415 .newJsonParser() 416 .setPrettyPrint(false); 417 String encoded = parser.encodeResourceToString(outcome); 418 if (encoded.length() > getMaximumHeaderLength()) { 419 encoded = encoded.substring(0, getMaximumHeaderLength() - 3) + "..."; 420 } 421 theRequestDetails.getResponse().addHeader(myResponseOutcomeHeaderName, encoded); 422 } 423 } 424 425 postProcessResult(theRequestDetails, validationResult); 426 427 return validationResult; 428 } 429 430 private static class MyLookup extends StrLookup<String> { 431 432 private SingleValidationMessage myMessage; 433 434 public MyLookup(SingleValidationMessage theMessage) { 435 myMessage = theMessage; 436 } 437 438 @Override 439 public String lookup(String theKey) { 440 if ("line".equals(theKey)) { 441 return toString(myMessage.getLocationLine()); 442 } 443 if ("col".equals(theKey)) { 444 return toString(myMessage.getLocationCol()); 445 } 446 if ("message".equals(theKey)) { 447 return toString(myMessage.getMessage()); 448 } 449 if ("location".equals(theKey)) { 450 return toString(myMessage.getLocationString()); 451 } 452 if ("severity".equals(theKey)) { 453 return myMessage.getSeverity() != null ? myMessage.getSeverity().name() : null; 454 } 455 456 return ""; 457 } 458 459 private static String toString(Object theInt) { 460 return theInt != null ? theInt.toString() : ""; 461 } 462 } 463}