001package ca.uhn.fhir.validation;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2021 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.interceptor.api.HookParams;
025import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
026import ca.uhn.fhir.interceptor.api.Pointcut;
027import ca.uhn.fhir.validation.schematron.SchematronProvider;
028import org.apache.commons.lang3.Validate;
029import org.hl7.fhir.instance.model.api.IBaseResource;
030
031import java.util.ArrayList;
032import java.util.Iterator;
033import java.util.List;
034
035
036/**
037 * Resource validator, which checks resources for compliance against various validation schemes (schemas, schematrons, profiles, etc.)
038 *
039 * <p>
040 * To obtain a resource validator, call {@link FhirContext#newValidator()}
041 * </p>
042 *
043 * <p>
044 * <b>Thread safety note:</b> This class is thread safe, so you may register or unregister validator modules at any time. Individual modules are not guaranteed to be thread safe however. Reconfigure
045 * them with caution.
046 * </p>
047 */
048public class FhirValidator {
049
050        private static final String I18N_KEY_NO_PH_ERROR = FhirValidator.class.getName() + ".noPhError";
051
052        private static volatile Boolean ourPhPresentOnClasspath;
053        private final FhirContext myContext;
054        private List<IValidatorModule> myValidators = new ArrayList<>();
055        private IInterceptorBroadcaster myInterceptorBraodcaster;
056
057        /**
058         * Constructor (this should not be called directly, but rather {@link FhirContext#newValidator()} should be called to obtain an instance of {@link FhirValidator})
059         */
060        public FhirValidator(FhirContext theFhirContext) {
061                myContext = theFhirContext;
062
063                if (ourPhPresentOnClasspath == null) {
064                        ourPhPresentOnClasspath = SchematronProvider.isSchematronAvailable(theFhirContext);
065                }
066        }
067
068        private void addOrRemoveValidator(boolean theValidateAgainstStandardSchema, Class<? extends IValidatorModule> type, IValidatorModule theInstance) {
069                if (theValidateAgainstStandardSchema) {
070                        boolean found = haveValidatorOfType(type);
071                        if (!found) {
072                                registerValidatorModule(theInstance);
073                        }
074                } else {
075                        for (Iterator<IValidatorModule> iter = myValidators.iterator(); iter.hasNext(); ) {
076                                IValidatorModule next = iter.next();
077                                if (next.getClass().equals(type)) {
078                                        unregisterValidatorModule(next);
079                                }
080                        }
081                }
082        }
083
084        private boolean haveValidatorOfType(Class<? extends IValidatorModule> type) {
085                boolean found = false;
086                for (IValidatorModule next : myValidators) {
087                        if (next.getClass().equals(type)) {
088                                found = true;
089                        }
090                }
091                return found;
092        }
093
094        /**
095         * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself)
096         */
097        public synchronized boolean isValidateAgainstStandardSchema() {
098                return haveValidatorOfType(SchemaBaseValidator.class);
099        }
100
101        /**
102         * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself)
103         *
104         * @return Returns a referens to <code>this<code> for method chaining
105         */
106        public synchronized FhirValidator setValidateAgainstStandardSchema(boolean theValidateAgainstStandardSchema) {
107                addOrRemoveValidator(theValidateAgainstStandardSchema, SchemaBaseValidator.class, new SchemaBaseValidator(myContext));
108                return this;
109        }
110
111        /**
112         * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself)
113         */
114        public synchronized boolean isValidateAgainstStandardSchematron() {
115                if (!ourPhPresentOnClasspath) {
116                        // No need to ask since we dont have Ph-Schematron. Also Class.forname will complain
117                        // about missing ph-schematron import.
118                        return false;
119                }
120                Class<? extends IValidatorModule> cls = SchematronProvider.getSchematronValidatorClass();
121                return haveValidatorOfType(cls);
122        }
123
124        /**
125         * Should the validator validate the resource against the base schematron (the schematron provided with the FHIR distribution itself)
126         *
127         * @return Returns a referens to <code>this<code> for method chaining
128         */
129        public synchronized FhirValidator setValidateAgainstStandardSchematron(boolean theValidateAgainstStandardSchematron) {
130                if (theValidateAgainstStandardSchematron && !ourPhPresentOnClasspath) {
131                        throw new IllegalArgumentException(myContext.getLocalizer().getMessage(I18N_KEY_NO_PH_ERROR));
132                }
133                if (!theValidateAgainstStandardSchematron && !ourPhPresentOnClasspath) {
134                        return this;
135                }
136                Class<? extends IValidatorModule> cls = SchematronProvider.getSchematronValidatorClass();
137                IValidatorModule instance = SchematronProvider.getSchematronValidatorInstance(myContext);
138                addOrRemoveValidator(theValidateAgainstStandardSchematron, cls, instance);
139                return this;
140        }
141
142        /**
143         * Add a new validator module to this validator. You may register as many modules as you like at any time.
144         *
145         * @param theValidator The validator module. Must not be null.
146         * @return Returns a reference to <code>this</code> for easy method chaining.
147         */
148        public synchronized FhirValidator registerValidatorModule(IValidatorModule theValidator) {
149                Validate.notNull(theValidator, "theValidator must not be null");
150                ArrayList<IValidatorModule> newValidators = new ArrayList<IValidatorModule>(myValidators.size() + 1);
151                newValidators.addAll(myValidators);
152                newValidators.add(theValidator);
153
154                myValidators = newValidators;
155                return this;
156        }
157
158        /**
159         * Removes a validator module from this validator. You may register as many modules as you like, and remove them at any time.
160         *
161         * @param theValidator The validator module. Must not be null.
162         */
163        public synchronized void unregisterValidatorModule(IValidatorModule theValidator) {
164                Validate.notNull(theValidator, "theValidator must not be null");
165                ArrayList<IValidatorModule> newValidators = new ArrayList<IValidatorModule>(myValidators.size() + 1);
166                newValidators.addAll(myValidators);
167                newValidators.remove(theValidator);
168
169                myValidators = newValidators;
170        }
171
172
173        private void applyDefaultValidators() {
174                if (myValidators.isEmpty()) {
175                        setValidateAgainstStandardSchema(true);
176                        if (ourPhPresentOnClasspath) {
177                                setValidateAgainstStandardSchematron(true);
178                        }
179                }
180        }
181
182
183        /**
184         * Validates a resource instance returning a {@link ValidationResult} which contains the results.
185         *
186         * @param theResource the resource to validate
187         * @return the results of validation
188         * @since 0.7
189         */
190        public ValidationResult validateWithResult(IBaseResource theResource) {
191                return validateWithResult(theResource, null);
192        }
193
194        /**
195         * Validates a resource instance returning a {@link ValidationResult} which contains the results.
196         *
197         * @param theResource the resource to validate
198         * @return the results of validation
199         * @since 1.1
200         */
201        public ValidationResult validateWithResult(String theResource) {
202                return validateWithResult(theResource, null);
203        }
204
205        /**
206         * Validates a resource instance returning a {@link ValidationResult} which contains the results.
207         *
208         * @param theResource the resource to validate
209         * @param theOptions  Optionally provides options to the validator
210         * @return the results of validation
211         * @since 4.0.0
212         */
213        public ValidationResult validateWithResult(IBaseResource theResource, ValidationOptions theOptions) {
214                Validate.notNull(theResource, "theResource must not be null");
215
216                applyDefaultValidators();
217
218                IValidationContext<IBaseResource> ctx = ValidationContext.forResource(myContext, theResource, theOptions);
219
220                for (IValidatorModule next : myValidators) {
221                        next.validateResource(ctx);
222                }
223
224                ValidationResult result = ctx.toResult();
225                result = invokeValidationCompletedHooks(theResource, null, result);
226                return result;
227        }
228
229        private ValidationResult invokeValidationCompletedHooks(IBaseResource theResourceParsed, String theResourceRaw, ValidationResult theValidationResult) {
230                if (myInterceptorBraodcaster != null) {
231                        if (myInterceptorBraodcaster.hasHooks(Pointcut.VALIDATION_COMPLETED)) {
232                                HookParams params = new HookParams()
233                                        .add(IBaseResource.class, theResourceParsed)
234                                        .add(String.class, theResourceRaw)
235                                        .add(ValidationResult.class, theValidationResult);
236                                Object newResult = myInterceptorBraodcaster.callHooksAndReturnObject(Pointcut.VALIDATION_COMPLETED, params);
237                                if (newResult != null) {
238                                        theValidationResult = (ValidationResult) newResult;
239                                }
240                        }
241                }
242                return theValidationResult;
243        }
244
245        /**
246         * Validates a resource instance returning a {@link ValidationResult} which contains the results.
247         *
248         * @param theResource the resource to validate
249         * @param theOptions  Optionally provides options to the validator
250         * @return the results of validation
251         * @since 4.0.0
252         */
253        public ValidationResult validateWithResult(String theResource, ValidationOptions theOptions) {
254                Validate.notNull(theResource, "theResource must not be null");
255
256                applyDefaultValidators();
257
258                IValidationContext<IBaseResource> ctx = ValidationContext.forText(myContext, theResource, theOptions);
259
260                for (IValidatorModule next : myValidators) {
261                        next.validateResource(ctx);
262                }
263
264                ValidationResult result = ctx.toResult();
265                result = invokeValidationCompletedHooks(null, theResource, result);
266                return result;
267        }
268
269        /**
270         * Optionally supplies an interceptor broadcaster that will be used to invoke validation related Pointcut events
271         *
272         * @since 5.5.0
273         */
274        public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBraodcaster) {
275                myInterceptorBraodcaster = theInterceptorBraodcaster;
276        }
277}