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