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