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}