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}