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