
001package ca.uhn.fhir.jpa.interceptor.validation; 002 003/*- 004 * #%L 005 * HAPI FHIR Storage api 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.context.FhirContext; 024import ca.uhn.fhir.context.support.IValidationSupport; 025import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 026import ca.uhn.fhir.jpa.validation.ValidatorPolicyAdvisor; 027import ca.uhn.fhir.jpa.validation.ValidatorResourceFetcher; 028import ca.uhn.fhir.rest.server.interceptor.ValidationResultEnrichingInterceptor; 029import ca.uhn.fhir.validation.ResultSeverityEnum; 030import org.apache.commons.lang3.Validate; 031import org.apache.commons.text.WordUtils; 032import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel; 033import org.springframework.beans.factory.annotation.Autowired; 034 035import javax.annotation.Nonnull; 036import java.util.ArrayList; 037import java.util.Arrays; 038import java.util.Collection; 039import java.util.List; 040 041import static com.google.common.base.Ascii.toLowerCase; 042import static org.apache.commons.lang3.StringUtils.isNotBlank; 043 044/** 045 * This class is used to construct rules to populate the {@link RepositoryValidatingInterceptor}. 046 * See <a href="https://hapifhir.io/hapi-fhir/docs/validation/repository_validating_interceptor.html">Repository Validating Interceptor</a> 047 * in the HAPI FHIR documentation for more information on how to use this. 048 */ 049public final class RepositoryValidatingRuleBuilder implements IRuleRoot { 050 051 public static final String REPOSITORY_VALIDATING_RULE_BUILDER = "repositoryValidatingRuleBuilder"; 052 private final List<IRepositoryValidatingRule> myRules = new ArrayList<>(); 053 054 @Autowired 055 private FhirContext myFhirContext; 056 private final IValidationSupport myValidationSupport; 057 @Autowired 058 private ValidatorResourceFetcher myValidatorResourceFetcher; 059 @Autowired 060 private ValidatorPolicyAdvisor myValidationPolicyAdvisor; 061 @Autowired 062 private IInterceptorBroadcaster myInterceptorBroadcaster; 063 064 public RepositoryValidatingRuleBuilder(IValidationSupport theValidationSupport) { 065 myValidationSupport = theValidationSupport; 066 } 067 068 /** 069 * Begin a new rule for a specific resource type. 070 * 071 * @param theType The resource type e.g. "Patient" (must not be null) 072 */ 073 @Override 074 public RepositoryValidatingRuleBuilderTyped forResourcesOfType(String theType) { 075 return new RepositoryValidatingRuleBuilderTyped(theType); 076 } 077 078 /** 079 * Create the repository validation rules 080 */ 081 @Override 082 public List<IRepositoryValidatingRule> build() { 083 return myRules; 084 } 085 086 public class FinalizedTypedRule implements IRuleRoot { 087 088 private final String myType; 089 090 FinalizedTypedRule(String theType) { 091 myType = theType; 092 } 093 094 @Override 095 public RepositoryValidatingRuleBuilderTyped forResourcesOfType(String theType) { 096 return RepositoryValidatingRuleBuilder.this.forResourcesOfType(theType); 097 } 098 099 @Override 100 public List<IRepositoryValidatingRule> build() { 101 return RepositoryValidatingRuleBuilder.this.build(); 102 } 103 104 public RepositoryValidatingRuleBuilderTyped and() { 105 return new RepositoryValidatingRuleBuilderTyped(myType); 106 } 107 } 108 109 public final class RepositoryValidatingRuleBuilderTyped { 110 111 private final String myType; 112 113 RepositoryValidatingRuleBuilderTyped(String theType) { 114 myType = myFhirContext.getResourceType(theType); 115 } 116 117 /** 118 * Require any resource being persisted to declare conformance to the given profile, meaning that the specified 119 * profile URL must be found within the resource in <code>Resource.meta.profile</code>. 120 * <p> 121 * This rule is non-exclusive, meaning that a resource will pass as long as one of its profile declarations 122 * in <code>Resource.meta.profile</code> matches. If the resource declares conformance to multiple profiles, any 123 * other profile declarations found in that field will be ignored. 124 * </p> 125 */ 126 public FinalizedTypedRule requireAtLeastProfile(String theProfileUrl) { 127 return requireAtLeastOneProfileOf(theProfileUrl); 128 } 129 130 /** 131 * Require any resource being persisted to declare conformance to at least one of the given profiles, meaning that the specified 132 * profile URL must be found within the resource in <code>Resource.meta.profile</code>. 133 * <p> 134 * This rule is non-exclusive, meaning that a resource will pass as long as one of its profile declarations 135 * in <code>Resource.meta.profile</code> matches. If the resource declares conformance to multiple profiles, any 136 * other profile declarations found in that field will be ignored. 137 * </p> 138 */ 139 public FinalizedTypedRule requireAtLeastOneProfileOf(String... theProfileUrls) { 140 Validate.notNull(theProfileUrls, "theProfileUrls must not be null"); 141 requireAtLeastOneProfileOf(Arrays.asList(theProfileUrls)); 142 return new FinalizedTypedRule(myType); 143 } 144 145 /** 146 * Require any resource being persisted to declare conformance to at least one of the given profiles, meaning that the specified 147 * profile URL must be found within the resource in <code>Resource.meta.profile</code>. 148 * <p> 149 * This rule is non-exclusive, meaning that a resource will pass as long as one of its profile declarations 150 * in <code>Resource.meta.profile</code> matches. If the resource declares conformance to multiple profiles, any 151 * other profile declarations found in that field will be ignored. 152 * </p> 153 */ 154 private FinalizedTypedRule requireAtLeastOneProfileOf(Collection<String> theProfileUrls) { 155 Validate.notNull(theProfileUrls, "theProfileUrls must not be null"); 156 Validate.notEmpty(theProfileUrls, "theProfileUrls must not be null or empty"); 157 myRules.add(new RuleRequireProfileDeclaration(myFhirContext, myType, theProfileUrls)); 158 return new FinalizedTypedRule(myType); 159 } 160 161 /** 162 * If set, any resources that contain a profile declaration in <code>Resource.meta.profile</code> 163 * matching {@literal theProfileUrl} will be rejected. 164 * 165 * @param theProfileUrl The profile canonical URL 166 */ 167 public FinalizedTypedRule disallowProfile(String theProfileUrl) { 168 return disallowProfiles(theProfileUrl); 169 } 170 171 /** 172 * Perform a resource validation step using the FHIR Instance Validator and reject the 173 * storage if the validation fails. 174 * 175 * <p> 176 * If the {@link ValidationResultEnrichingInterceptor} is registered against the 177 * {@link ca.uhn.fhir.rest.server.RestfulServer} interceptor registry, the validation results 178 * will be appended to any <code>OperationOutcome</code> resource returned by the server. 179 * </p> 180 * 181 * @see ValidationResultEnrichingInterceptor 182 */ 183 public FinalizedRequireValidationRule requireValidationToDeclaredProfiles() { 184 RequireValidationRule rule = new RequireValidationRule(myFhirContext, myType, myValidationSupport, 185 myValidatorResourceFetcher, myValidationPolicyAdvisor, myInterceptorBroadcaster); 186 myRules.add(rule); 187 return new FinalizedRequireValidationRule(rule); 188 } 189 190 public FinalizedTypedRule disallowProfiles(String... theProfileUrls) { 191 Validate.notNull(theProfileUrls, "theProfileUrl must not be null or empty"); 192 Validate.notEmpty(theProfileUrls, "theProfileUrl must not be null or empty"); 193 myRules.add(new RuleDisallowProfile(myFhirContext, myType, theProfileUrls)); 194 return new FinalizedTypedRule(myType); 195 } 196 197 198 public class FinalizedRequireValidationRule extends FinalizedTypedRule { 199 200 private final RequireValidationRule myRule; 201 202 public FinalizedRequireValidationRule(RequireValidationRule theRule) { 203 super(myType); 204 myRule = theRule; 205 } 206 207 /** 208 * Sets the "Best Practice Warning Level", which is the severity at which any "best practices" that 209 * are specified in the FHIR specification will be added to the validation outcome. Set to 210 * <code>ERROR</code> to cause any best practice notices to result in a validation failure. 211 * Set to <code>IGNORE</code> to not include any best practice notifications. 212 */ 213 @Nonnull 214 public FinalizedRequireValidationRule withBestPracticeWarningLevel(String theBestPracticeWarningLevel) { 215 BestPracticeWarningLevel level = null; 216 if (isNotBlank(theBestPracticeWarningLevel)) { 217 level = BestPracticeWarningLevel.valueOf(WordUtils.capitalize(theBestPracticeWarningLevel.toLowerCase())); 218 } 219 return withBestPracticeWarningLevel(level); 220 } 221 222 /** 223 * Sets the "Best Practice Warning Level", which is the severity at which any "best practices" that 224 * are specified in the FHIR specification will be added to the validation outcome. Set to 225 * {@link org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel#Error} to 226 * cause any best practice notices to result in a validation failure. 227 * Set to {@link org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel#Ignore} 228 * to not include any best practice notifications. 229 */ 230 @Nonnull 231 public FinalizedRequireValidationRule withBestPracticeWarningLevel(BestPracticeWarningLevel bestPracticeWarningLevel) { 232 myRule.setBestPracticeWarningLevel(bestPracticeWarningLevel); 233 return this; 234 } 235 236 /** 237 * Specifies that the resource should not be rejected from storage even if it does not pass validation. 238 */ 239 @Nonnull 240 public FinalizedRequireValidationRule neverReject() { 241 myRule.dontReject(); 242 return this; 243 } 244 245 /** 246 * Specifies the minimum validation result severity that should cause a rejection. For example, if 247 * this is set to <code>ERROR</code> (which is the default), any validation results with a severity 248 * of <code>ERROR</code> or <code>FATAL</code> will cause the create/update operation to be rejected and 249 * rolled back, and no data will be saved. 250 * <p> 251 * Valid values must be drawn from {@link ResultSeverityEnum} 252 * </p> 253 */ 254 @Nonnull 255 public FinalizedRequireValidationRule rejectOnSeverity(@Nonnull String theSeverity) { 256 ResultSeverityEnum severity = ResultSeverityEnum.fromCode(toLowerCase(theSeverity)); 257 Validate.notNull(severity, "Invalid severity code: %s", theSeverity); 258 return rejectOnSeverity(severity); 259 } 260 261 /** 262 * Specifies the minimum validation result severity that should cause a rejection. For example, if 263 * Specifies the minimum validation result severity that should cause a rejection. For example, if 264 * this is set to <code>ERROR</code> (which is the default), any validation results with a severity 265 * of <code>ERROR</code> or <code>FATAL</code> will cause the create/update operation to be rejected and 266 * rolled back, and no data will be saved. 267 * <p> 268 * Valid values must be drawn from {@link ResultSeverityEnum} 269 * </p> 270 */ 271 @Nonnull 272 public FinalizedRequireValidationRule rejectOnSeverity(@Nonnull ResultSeverityEnum theSeverity) { 273 myRule.rejectOnSeverity(theSeverity); 274 return this; 275 } 276 277 /** 278 * Specifies that if the validation results in any results with a severity of <code>theSeverity</code> or 279 * greater, the resource will be tagged with the given tag when it is saved. 280 * 281 * @param theSeverity The minimum severity. Must be drawn from values in {@link ResultSeverityEnum} and must not be <code>null</code> 282 * @param theTagSystem The system for the tag to add. Must not be <code>null</code> 283 * @param theTagCode The code for the tag to add. Must not be <code>null</code> 284 * @return 285 */ 286 @Nonnull 287 public FinalizedRequireValidationRule tagOnSeverity(@Nonnull String theSeverity, @Nonnull String theTagSystem, @Nonnull String theTagCode) { 288 ResultSeverityEnum severity = ResultSeverityEnum.fromCode(toLowerCase(theSeverity)); 289 return tagOnSeverity(severity, theTagSystem, theTagCode); 290 } 291 292 /** 293 * Specifies that if the validation results in any results with a severity of <code>theSeverity</code> or 294 * greater, the resource will be tagged with the given tag when it is saved. 295 * 296 * @param theSeverity The minimum severity. Must be drawn from values in {@link ResultSeverityEnum} and must not be <code>null</code> 297 * @param theTagSystem The system for the tag to add. Must not be <code>null</code> 298 * @param theTagCode The code for the tag to add. Must not be <code>null</code> 299 * @return 300 */ 301 @Nonnull 302 public FinalizedRequireValidationRule tagOnSeverity(@Nonnull ResultSeverityEnum theSeverity, @Nonnull String theTagSystem, @Nonnull String theTagCode) { 303 myRule.tagOnSeverity(theSeverity, theTagSystem, theTagCode); 304 return this; 305 } 306 307 /** 308 * Configure the validator to never reject extensions 309 */ 310 @Nonnull 311 public FinalizedRequireValidationRule allowAnyExtensions() { 312 myRule.getValidator().setAnyExtensionsAllowed(true); 313 return this; 314 } 315 316 /** 317 * Configure the validator to reject unknown extensions 318 */ 319 @Nonnull 320 public FinalizedRequireValidationRule rejectUnknownExtensions() { 321 myRule.getValidator().setAnyExtensionsAllowed(false); 322 return this; 323 } 324 325 /** 326 * Configure the validator to not perform terminology validation 327 */ 328 @Nonnull 329 public FinalizedRequireValidationRule disableTerminologyChecks() { 330 myRule.getValidator().setNoTerminologyChecks(true); 331 return this; 332 } 333 334 /** 335 * Configure the validator to raise an error if a resource being validated 336 * declares a profile, and the StructureDefinition for this profile 337 * can not be found. 338 */ 339 @Nonnull 340 public FinalizedRequireValidationRule errorOnUnknownProfiles() { 341 myRule.getValidator().setErrorForUnknownProfiles(true); 342 return this; 343 } 344 345 /** 346 * Configure the validator to suppress the information-level message that 347 * is added to the validation result if a profile StructureDefinition does 348 * not declare a binding for a coded field. 349 */ 350 @Nonnull 351 public FinalizedRequireValidationRule suppressNoBindingMessage() { 352 myRule.getValidator().setNoBindingMsgSuppressed(true); 353 return this; 354 } 355 356 /** 357 * Configure the validator to suppress the warning-level message that 358 * is added when validating a code that can't be found in an ValueSet that 359 * has an extensible binding. 360 */ 361 @Nonnull 362 public FinalizedRequireValidationRule suppressWarningForExtensibleValueSetValidation() { 363 myRule.getValidator().setNoExtensibleWarnings(true); 364 return this; 365 } 366 367 } 368 369 } 370 371}