001package ca.uhn.fhir.validation;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
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.i18n.Msg;
025import ca.uhn.fhir.interceptor.api.HookParams;
026import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
027import ca.uhn.fhir.interceptor.api.Pointcut;
028import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
029import ca.uhn.fhir.util.BundleUtil;
030import ca.uhn.fhir.util.TerserUtil;
031import ca.uhn.fhir.validation.schematron.SchematronProvider;
032import org.apache.commons.lang3.StringUtils;
033import org.apache.commons.lang3.Validate;
034import org.hl7.fhir.instance.model.api.IBaseBundle;
035import org.hl7.fhir.instance.model.api.IBaseResource;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039import java.util.ArrayList;
040import java.util.List;
041import java.util.concurrent.ExecutionException;
042import java.util.concurrent.ExecutorService;
043import java.util.concurrent.Future;
044import java.util.function.Function;
045import java.util.stream.Collectors;
046import java.util.stream.IntStream;
047
048import static org.apache.commons.lang3.StringUtils.isBlank;
049
050
051/**
052 * Resource validator, which checks resources for compliance against various validation schemes (schemas, schematrons, profiles, etc.)
053 *
054 * <p>
055 * To obtain a resource validator, call {@link FhirContext#newValidator()}
056 * </p>
057 *
058 * <p>
059 * <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
060 * them with caution.
061 * </p>
062 */
063public class FhirValidator {
064        private static final Logger ourLog = LoggerFactory.getLogger(FhirValidator.class);
065
066        private static final String I18N_KEY_NO_PH_ERROR = FhirValidator.class.getName() + ".noPhError";
067
068        private static volatile Boolean ourPhPresentOnClasspath;
069        private final FhirContext myContext;
070        private List<IValidatorModule> myValidators = new ArrayList<>();
071        private IInterceptorBroadcaster myInterceptorBroadcaster;
072        private boolean myConcurrentBundleValidation;
073        private boolean mySkipContainedReferenceValidation;
074
075        private ExecutorService myExecutorService;
076
077        /**
078         * Constructor (this should not be called directly, but rather {@link FhirContext#newValidator()} should be called to obtain an instance of {@link FhirValidator})
079         */
080        public FhirValidator(FhirContext theFhirContext) {
081                myContext = theFhirContext;
082
083                if (ourPhPresentOnClasspath == null) {
084                        ourPhPresentOnClasspath = SchematronProvider.isSchematronAvailable(theFhirContext);
085                }
086        }
087
088        private void addOrRemoveValidator(boolean theValidateAgainstStandardSchema, Class<? extends IValidatorModule> type, IValidatorModule theInstance) {
089                if (theValidateAgainstStandardSchema) {
090                        boolean found = haveValidatorOfType(type);
091                        if (!found) {
092                                registerValidatorModule(theInstance);
093                        }
094                } else {
095                        for (IValidatorModule next : myValidators) {
096                                if (next.getClass().equals(type)) {
097                                        unregisterValidatorModule(next);
098                                }
099                        }
100                }
101        }
102
103        private boolean haveValidatorOfType(Class<? extends IValidatorModule> type) {
104                boolean found = false;
105                for (IValidatorModule next : myValidators) {
106                        if (next.getClass().equals(type)) {
107                                found = true;
108                                break;
109                        }
110                }
111                return found;
112        }
113
114        /**
115         * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself)
116         */
117        public synchronized boolean isValidateAgainstStandardSchema() {
118                return haveValidatorOfType(SchemaBaseValidator.class);
119        }
120
121        /**
122         * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself)
123         *
124         * @return Returns a referens to <code>this<code> for method chaining
125         */
126        public synchronized FhirValidator setValidateAgainstStandardSchema(boolean theValidateAgainstStandardSchema) {
127                addOrRemoveValidator(theValidateAgainstStandardSchema, SchemaBaseValidator.class, new SchemaBaseValidator(myContext));
128                return this;
129        }
130
131        /**
132         * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself)
133         */
134        public synchronized boolean isValidateAgainstStandardSchematron() {
135                if (!ourPhPresentOnClasspath) {
136                        // No need to ask since we dont have Ph-Schematron. Also Class.forname will complain
137                        // about missing ph-schematron import.
138                        return false;
139                }
140                Class<? extends IValidatorModule> cls = SchematronProvider.getSchematronValidatorClass();
141                return haveValidatorOfType(cls);
142        }
143
144        /**
145         * Should the validator validate the resource against the base schematron (the schematron provided with the FHIR distribution itself)
146         *
147         * @return Returns a referens to <code>this<code> for method chaining
148         */
149        public synchronized FhirValidator setValidateAgainstStandardSchematron(boolean theValidateAgainstStandardSchematron) {
150                if (theValidateAgainstStandardSchematron && !ourPhPresentOnClasspath) {
151                        throw new IllegalArgumentException(Msg.code(1970) + myContext.getLocalizer().getMessage(I18N_KEY_NO_PH_ERROR));
152                }
153                if (!theValidateAgainstStandardSchematron && !ourPhPresentOnClasspath) {
154                        return this;
155                }
156                Class<? extends IValidatorModule> cls = SchematronProvider.getSchematronValidatorClass();
157                IValidatorModule instance = SchematronProvider.getSchematronValidatorInstance(myContext);
158                addOrRemoveValidator(theValidateAgainstStandardSchematron, cls, instance);
159                return this;
160        }
161
162        /**
163         * Add a new validator module to this validator. You may register as many modules as you like at any time.
164         *
165         * @param theValidator The validator module. Must not be null.
166         * @return Returns a reference to <code>this</code> for easy method chaining.
167         */
168        public synchronized FhirValidator registerValidatorModule(IValidatorModule theValidator) {
169                Validate.notNull(theValidator, "theValidator must not be null");
170                ArrayList<IValidatorModule> newValidators = new ArrayList<IValidatorModule>(myValidators.size() + 1);
171                newValidators.addAll(myValidators);
172                newValidators.add(theValidator);
173
174                myValidators = newValidators;
175                return this;
176        }
177
178        /**
179         * Removes a validator module from this validator. You may register as many modules as you like, and remove them at any time.
180         *
181         * @param theValidator The validator module. Must not be null.
182         */
183        public synchronized void unregisterValidatorModule(IValidatorModule theValidator) {
184                Validate.notNull(theValidator, "theValidator must not be null");
185                ArrayList<IValidatorModule> newValidators = new ArrayList<IValidatorModule>(myValidators.size() + 1);
186                newValidators.addAll(myValidators);
187                newValidators.remove(theValidator);
188
189                myValidators = newValidators;
190        }
191
192
193        private void applyDefaultValidators() {
194                if (myValidators.isEmpty()) {
195                        setValidateAgainstStandardSchema(true);
196                        if (ourPhPresentOnClasspath) {
197                                setValidateAgainstStandardSchematron(true);
198                        }
199                }
200        }
201
202
203        /**
204         * Validates a resource instance returning a {@link ValidationResult} which contains the results.
205         *
206         * @param theResource the resource to validate
207         * @return the results of validation
208         * @since 0.7
209         */
210        public ValidationResult validateWithResult(IBaseResource theResource) {
211                return validateWithResult(theResource, null);
212        }
213
214        /**
215         * Validates a resource instance returning a {@link ValidationResult} which contains the results.
216         *
217         * @param theResource the resource to validate
218         * @return the results of validation
219         * @since 1.1
220         */
221        public ValidationResult validateWithResult(String theResource) {
222                return validateWithResult(theResource, null);
223        }
224
225        /**
226         * Validates a resource instance returning a {@link ValidationResult} which contains the results.
227         *
228         * @param theResource the resource to validate
229         * @param theOptions  Optionally provides options to the validator
230         * @return the results of validation
231         * @since 4.0.0
232         */
233        public ValidationResult validateWithResult(String theResource, ValidationOptions theOptions) {
234                Validate.notNull(theResource, "theResource must not be null");
235                IValidationContext<IBaseResource> validationContext = ValidationContext.forText(myContext, theResource, theOptions);
236                Function<ValidationResult, ValidationResult> callback = result -> invokeValidationCompletedHooks(null, theResource, result);
237                return doValidate(validationContext, theOptions, callback);
238        }
239
240        /**
241         * Validates a resource instance returning a {@link ValidationResult} which contains the results.
242         *
243         * @param theResource the resource to validate
244         * @param theOptions  Optionally provides options to the validator
245         * @return the results of validation
246         * @since 4.0.0
247         */
248        public ValidationResult validateWithResult(IBaseResource theResource, ValidationOptions theOptions) {
249                Validate.notNull(theResource, "theResource must not be null");
250                IValidationContext<IBaseResource> validationContext = ValidationContext.forResource(myContext, theResource, theOptions);
251                Function<ValidationResult, ValidationResult> callback = result -> invokeValidationCompletedHooks(theResource, null, result);
252                return doValidate(validationContext, theOptions, callback);
253        }
254
255        private ValidationResult doValidate(IValidationContext<IBaseResource> theValidationContext, ValidationOptions theOptions,
256                                                                                                        Function<ValidationResult, ValidationResult> theValidationCompletionCallback) {
257                applyDefaultValidators();
258
259                ValidationResult result;
260                if (myConcurrentBundleValidation && theValidationContext.getResource() instanceof IBaseBundle
261                        && myExecutorService != null) {
262                        result = validateBundleEntriesConcurrently(theValidationContext, theOptions);
263                } else {
264                        result = validateResource(theValidationContext);
265                }
266
267                return theValidationCompletionCallback.apply(result);
268        }
269
270        private ValidationResult validateBundleEntriesConcurrently(IValidationContext<IBaseResource> theValidationContext, ValidationOptions theOptions) {
271                List<IBaseResource> entries = BundleUtil.toListOfResources(myContext, (IBaseBundle) theValidationContext.getResource());
272                // Async validation tasks
273                List<ConcurrentValidationTask> validationTasks = IntStream.range(0, entries.size())
274                        .mapToObj(index -> {
275                                IBaseResource resourceToValidate;
276                                IBaseResource entry = entries.get(index);
277
278                                if (mySkipContainedReferenceValidation) {
279                                        resourceToValidate = withoutContainedResources(entry);
280                                } else {
281                                        resourceToValidate = entry;
282                                }
283
284                                String entryPathPrefix = String.format("Bundle.entry[%d].resource.ofType(%s)", index, resourceToValidate.fhirType());
285                                Future<ValidationResult> future = myExecutorService.submit(() -> {
286                                        IValidationContext<IBaseResource> entryValidationContext = ValidationContext.forResource(theValidationContext.getFhirContext(), resourceToValidate, theOptions);
287                                        return validateResource(entryValidationContext);
288                                });
289                                return new ConcurrentValidationTask(entryPathPrefix, future);
290                        }).collect(Collectors.toList());
291
292                List<SingleValidationMessage> validationMessages = buildValidationMessages(validationTasks);
293                return new ValidationResult(myContext, validationMessages);
294        }
295
296        IBaseResource withoutContainedResources(IBaseResource theEntry) {
297                if (TerserUtil.hasValues(myContext, theEntry, "contained")) {
298                        IBaseResource deepCopy = TerserUtil.clone(myContext, theEntry);
299                        TerserUtil.clearField(myContext, deepCopy, "contained");
300                        return deepCopy;
301                } else {
302                        return theEntry;
303                }
304        }
305
306        static List<SingleValidationMessage> buildValidationMessages(List<ConcurrentValidationTask> validationTasks) {
307                List<SingleValidationMessage> retval = new ArrayList<>();
308                try {
309                        for (ConcurrentValidationTask validationTask : validationTasks) {
310                                ValidationResult result = validationTask.getFuture().get();
311                                final String bundleEntryPathPrefix = validationTask.getResourcePathPrefix();
312                                List<SingleValidationMessage> messages = result.getMessages().stream()
313                                        .map(message -> {
314                                                String currentPath;
315
316                                                String locationString = StringUtils.defaultIfEmpty(message.getLocationString(), "");
317
318                                                int dotIndex = locationString.indexOf('.');
319                                                if (dotIndex >= 0) {
320                                                        currentPath = locationString.substring(dotIndex);
321                                                } else {
322                                                        if (isBlank(bundleEntryPathPrefix) || isBlank(locationString)) {
323                                                                currentPath = locationString;
324                                                        } else {
325                                                                currentPath = "." + locationString;
326                                                        }
327                                                }
328
329                                                message.setLocationString(bundleEntryPathPrefix + currentPath);
330                                                return message;
331                                        })
332                                        .collect(Collectors.toList());
333                                retval.addAll(messages);
334                        }
335                } catch (InterruptedException | ExecutionException exp) {
336                        throw new InternalErrorException(Msg.code(1975) + exp);
337                }
338                return retval;
339        }
340
341        private ValidationResult validateResource(IValidationContext<IBaseResource> theValidationContext) {
342                for (IValidatorModule next : myValidators) {
343                        next.validateResource(theValidationContext);
344                }
345                return theValidationContext.toResult();
346        }
347
348        private ValidationResult invokeValidationCompletedHooks(IBaseResource theResourceParsed, String theResourceRaw, ValidationResult theValidationResult) {
349                if (myInterceptorBroadcaster != null) {
350                        if (myInterceptorBroadcaster.hasHooks(Pointcut.VALIDATION_COMPLETED)) {
351                                HookParams params = new HookParams()
352                                        .add(IBaseResource.class, theResourceParsed)
353                                        .add(String.class, theResourceRaw)
354                                        .add(ValidationResult.class, theValidationResult);
355                                Object newResult = myInterceptorBroadcaster.callHooksAndReturnObject(Pointcut.VALIDATION_COMPLETED, params);
356                                if (newResult != null) {
357                                        theValidationResult = (ValidationResult) newResult;
358                                }
359                        }
360                }
361                return theValidationResult;
362        }
363
364        /**
365         * Optionally supplies an interceptor broadcaster that will be used to invoke validation related Pointcut events
366         *
367         * @since 5.5.0
368         */
369        public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBraodcaster) {
370                myInterceptorBroadcaster = theInterceptorBraodcaster;
371        }
372
373        public FhirValidator setExecutorService(ExecutorService theExecutorService) {
374                myExecutorService = theExecutorService;
375                return this;
376        }
377
378        /**
379         * If this is true, bundles will be validated in parallel threads.  The bundle structure itself will not be validated,
380         * only the resources in its entries.
381         */
382
383        public boolean isConcurrentBundleValidation() {
384                return myConcurrentBundleValidation;
385        }
386
387        /**
388         * If this is true, bundles will be validated in parallel threads.  The bundle structure itself will not be validated,
389         * only the resources in its entries.
390         */
391        public FhirValidator setConcurrentBundleValidation(boolean theConcurrentBundleValidation) {
392                myConcurrentBundleValidation = theConcurrentBundleValidation;
393                return this;
394        }
395
396        /**
397         * If this is true, any resource that has contained resources will first be deep-copied and then the contained
398         * resources remove from the copy and this copy without contained resources will be validated.
399         */
400        public boolean isSkipContainedReferenceValidation() {
401                return mySkipContainedReferenceValidation;
402        }
403
404        /**
405         * If this is true, any resource that has contained resources will first be deep-copied and then the contained
406         * resources remove from the copy and this copy without contained resources will be validated.
407         */
408        public FhirValidator setSkipContainedReferenceValidation(boolean theSkipContainedReferenceValidation) {
409                mySkipContainedReferenceValidation = theSkipContainedReferenceValidation;
410                return this;
411        }
412
413        // Simple Tuple to keep track of bundle path and associate aync future task
414        static class ConcurrentValidationTask {
415                private final String myResourcePathPrefix;
416                private final Future<ValidationResult> myFuture;
417
418                ConcurrentValidationTask(String theResourcePathPrefix, Future<ValidationResult> theFuture) {
419                        myResourcePathPrefix = theResourcePathPrefix;
420                        myFuture = theFuture;
421                }
422
423                public String getResourcePathPrefix() {
424                        return myResourcePathPrefix;
425                }
426
427                public Future<ValidationResult> getFuture() {
428                        return myFuture;
429                }
430        }
431
432}