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}