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