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